From e011656afc6fe1c9aff5b1bd3758ff1809bec6dd Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 7 Jul 2026 15:54:24 -0700 Subject: [PATCH 01/17] migrate $map, $zip, $range tests Co-authored-by: Leszek Kurzyna Co-authored-by: Sean Oczkowski Signed-off-by: Alina (Xi) Li --- .../expressions/array/map/__init__.py | 0 .../map/test_expression_map_as_errors.py | 128 ++++ .../map/test_expression_map_bson_types.py | 291 ++++++++ .../map/test_expression_map_core_behavior.py | 333 +++++++++ .../array/map/test_expression_map_errors.py | 287 ++++++++ .../map/test_expression_map_expressions.py | 255 +++++++ .../test_expression_map_structure_errors.py | 119 +++ .../array/map/test_smoke_expression_map.py | 2 +- .../expressions/array/range/__init__.py | 0 .../range/test_expression_range_boundary.py | 110 +++ .../test_expression_range_core_behavior.py | 496 +++++++++++++ .../range/test_expression_range_errors.py | 695 ++++++++++++++++++ .../test_expression_range_expressions.py | 169 +++++ .../range/test_smoke_expression_range.py | 2 +- .../expressions/array/range/utils/__init__.py | 0 .../array/range/utils/range_common.py | 17 + .../expressions/array/zip/__init__.py | 0 ...xpression_zip_argument_structure_errors.py | 133 ++++ .../zip/test_expression_zip_bson_types.py | 331 +++++++++ .../zip/test_expression_zip_core_behavior.py | 442 +++++++++++ .../array/zip/test_expression_zip_errors.py | 440 +++++++++++ .../zip/test_expression_zip_expressions.py | 250 +++++++ .../array/zip/test_smoke_expression_zip.py | 2 +- .../expressions/array/zip/utils/__init__.py | 0 .../expressions/array/zip/utils/zip_common.py | 17 + .../test_expressions_combination_map.py | 94 +++ .../test_expressions_combination_range.py | 87 +++ .../test_expressions_combination_zip.py | 124 ++++ 28 files changed, 4821 insertions(+), 3 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/map/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_as_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_bson_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_structure_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/range/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_boundary.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/range_common.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_argument_structure_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_bson_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/zip_common.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_map.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_range.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_zip.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_as_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_as_errors.py new file mode 100644 index 000000000..9e3f9d638 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_as_errors.py @@ -0,0 +1,128 @@ +""" +Error tests for $map 'as' parameter. + +Tests invalid 'as' types, invalid variable names, and reserved variable names. +""" + +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 ( # noqa: E501 + execute_expression, +) +from documentdb_tests.framework.assertions import assertResult +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={"$map": {"input": [1, 2, 3], "as": 1, "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=int should error", + ), + ExpressionTestCase( + id="type_long", + expression={"$map": {"input": [1, 2, 3], "as": Int64(1), "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=Int64 should error", + ), + ExpressionTestCase( + id="type_object", + expression={"$map": {"input": [1, 2, 3], "as": {"a": 1}, "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=object should error", + ), + ExpressionTestCase( + id="type_array", + expression={"$map": {"input": [1, 2, 3], "as": [1], "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=array should error", + ), + ExpressionTestCase( + id="type_minkey", + expression={"$map": {"input": [1, 2, 3], "as": MinKey(), "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=MinKey should error", + ), + ExpressionTestCase( + id="type_maxkey", + expression={"$map": {"input": [1, 2, 3], "as": MaxKey(), "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=MaxKey should error", + ), + ExpressionTestCase( + id="type_bindata", + expression={"$map": {"input": [1, 2, 3], "as": Binary(b"\x00", 0), "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=Binary should error", + ), + ExpressionTestCase( + id="type_objectid", + expression={"$map": {"input": [1, 2, 3], "as": ObjectId(), "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=ObjectId should error", + ), + ExpressionTestCase( + id="type_date", + expression={"$map": {"input": [1, 2, 3], "as": datetime(2026, 1, 1), "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=datetime should error", + ), + ExpressionTestCase( + id="type_timestamp", + expression={"$map": {"input": [1, 2, 3], "as": Timestamp(0, 0), "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=Timestamp should error", + ), + ExpressionTestCase( + id="type_regex", + expression={"$map": {"input": [1, 2, 3], "as": Regex("pattern"), "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=Regex should error", + ), + ExpressionTestCase( + id="type_bool_true", + expression={"$map": {"input": [1, 2, 3], "as": True, "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=True should error", + ), + ExpressionTestCase( + id="type_null", + expression={"$map": {"input": [1, 2, 3], "as": None, "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=None should error", + ), + ExpressionTestCase( + id="type_empty_string", + expression={"$map": {"input": [1, 2, 3], "as": "", "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as='' should error", + ), + ExpressionTestCase( + id="type_nan", + expression={"$map": {"input": [1, 2, 3], "as": float("nan"), "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=NaN should error", + ), + ExpressionTestCase( + id="type_infinity", + expression={"$map": {"input": [1, 2, 3], "as": float("inf"), "in": "$$this"}}, + error_code=FAILED_TO_PARSE_ERROR, + msg="as=inf should error", + ), +] + +ALL_AS_TESTS = INVALID_AS_TYPE_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_AS_TESTS)) +def test_map_invalid_as(collection, test): + """Test $map with invalid 'as' parameter values.""" + result = execute_expression(collection, test.expression) + assertResult(result, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_bson_types.py new file mode 100644 index 000000000..c6faa908e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_bson_types.py @@ -0,0 +1,291 @@ +""" +BSON type element preservation tests for $map expression. + +Tests that various BSON types are preserved when mapping over arrays, +including special numeric values and boundary values. +""" + +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.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +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 via identity map +# --------------------------------------------------------------------------- +BSON_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="int64_values", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + 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={"$map": {"input": "$arr", "in": "$$this"}}, + 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={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": [datetime(2024, 1, 1), datetime(2024, 6, 1)]}, + expected=[datetime(2024, 1, 1), datetime(2024, 6, 1)], + msg="Should preserve datetime values", + ), + ExpressionTestCase( + id="objectid_values", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": [ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")]}, + expected=[ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")], + msg="Should preserve ObjectId values", + ), + ExpressionTestCase( + id="binary_values", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + 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={"$map": {"input": "$arr", "in": "$$this"}}, + 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={"$map": {"input": "$arr", "in": "$$this"}}, + 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={"$map": {"input": "$arr", "in": "$$this"}}, + 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={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": [MinKey(), MaxKey()]}, + expected=[MinKey(), MaxKey()], + msg="Should preserve MinKey/MaxKey values", + ), + ExpressionTestCase( + id="uuid_values", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + 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={"$map": {"input": "$arr", "in": "$$this"}}, + 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 via identity", + ), + ExpressionTestCase( + id="mixed_dates_and_ids", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": [datetime(2024, 1, 1), ObjectId("000000000000000000000001"), Timestamp(1, 0)]}, + expected=[datetime(2024, 1, 1), 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={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": [FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY]}, + expected=[FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY], + msg="Should preserve infinity values", + ), + ExpressionTestCase( + id="decimal128_infinity", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": [DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY]}, + expected=[DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY], + msg="Should preserve Decimal128 infinity values", + ), + ExpressionTestCase( + id="boundary_values", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + 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={"$map": {"input": "$arr", "in": "$$this"}}, + 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={"$map": {"input": "$arr", "in": "$$this"}}, + 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={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": [DECIMAL128_NAN, Decimal128("1")]}, + expected=[DECIMAL128_NAN, Decimal128("1")], + msg="Decimal128 NaN preserved", + ), +] + +# --------------------------------------------------------------------------- +# BSON type transformations +# --------------------------------------------------------------------------- +BSON_TRANSFORM_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="multiply_int64", + expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", Int64(2)]}}}, + doc={"arr": [Int64(10), Int64(20), Int64(30)]}, + expected=[Int64(20), Int64(40), Int64(60)], + msg="$multiply on Int64 should preserve Int64 type", + ), + ExpressionTestCase( + id="add_decimal128", + expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", Decimal128("0.1")]}}}, + doc={"arr": [Decimal128("1.5"), Decimal128("2.5"), Decimal128("3.5")]}, + expected=[Decimal128("1.6"), Decimal128("2.6"), Decimal128("3.6")], + msg="$add on Decimal128 should preserve precision", + ), + ExpressionTestCase( + id="type_of_mixed_bson", + expression={"$map": {"input": "$arr", "in": {"$type": "$$this"}}}, + doc={"arr": [1, "two", Int64(3), Decimal128("4"), True, None]}, + expected=["int", "string", "long", "decimal", "bool", "null"], + msg="$type on mixed BSON types", + ), + ExpressionTestCase( + id="dateToString_datetime", + expression={ + "$map": { + "input": "$arr", + "in": {"$dateToString": {"format": "%Y-%m-%d", "date": "$$this"}}, + } + }, + doc={"arr": [datetime(2024, 1, 1), datetime(2024, 6, 15)]}, + expected=["2024-01-01", "2024-06-15"], + msg="$dateToString on datetime array", + ), + ExpressionTestCase( + id="toString_objectid", + expression={"$map": {"input": "$arr", "in": {"$toString": "$$this"}}}, + doc={"arr": [ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")]}, + expected=["000000000000000000000001", "000000000000000000000002"], + msg="$toString on ObjectId array", + ), + ExpressionTestCase( + id="toLong_int_values", + expression={"$map": {"input": "$arr", "in": {"$toLong": "$$this"}}}, + doc={"arr": [1, 2, 3]}, + expected=[Int64(1), Int64(2), Int64(3)], + msg="$toLong converts int to Int64", + ), + ExpressionTestCase( + id="toDouble_decimal128", + expression={"$map": {"input": "$arr", "in": {"$toDouble": "$$this"}}}, + doc={"arr": [Decimal128("1.5"), Decimal128("2.0")]}, + expected=[1.5, 2.0], + msg="$toDouble on Decimal128 array", + ), + ExpressionTestCase( + id="concat_strings", + expression={"$map": {"input": "$arr", "in": {"$concat": ["$$this", "!"]}}}, + doc={"arr": ["hello", "world"]}, + expected=["hello!", "world!"], + msg="$concat on string array", + ), + ExpressionTestCase( + id="add_millis_to_datetime", + expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 86400000]}}}, + doc={"arr": [datetime(2024, 1, 1), datetime(2024, 6, 1)]}, + expected=[datetime(2024, 1, 2), datetime(2024, 6, 2)], + msg="Add one day in millis to datetime array", + ), + ExpressionTestCase( + id="subtract_int64", + expression={"$map": {"input": "$arr", "in": {"$subtract": ["$$this", Int64(50)]}}}, + doc={"arr": [Int64(100), Int64(200), Int64(300)]}, + expected=[Int64(50), Int64(150), Int64(250)], + msg="$subtract on Int64 array", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_BSON_TESTS = ( + BSON_TYPE_TESTS + + MIXED_BSON_TESTS + + SPECIAL_NUMERIC_TESTS + + DECIMAL128_PRECISION_TESTS + + BSON_TRANSFORM_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_BSON_TESTS)) +def test_map_bson_insert(collection, test): + """Test $map BSON types with values from inserted documents.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assertResult(result, expected=test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_core_behavior.py new file mode 100644 index 000000000..6fa5369d4 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_core_behavior.py @@ -0,0 +1,333 @@ +""" +Core behavior tests for $map expression. + +Tests basic mapping, identity transforms, nested arrays, null propagation, +empty arrays, various in expressions, default and custom 'as' variable, +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 ( # noqa: E501 + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Success: basic mapping +# --------------------------------------------------------------------------- +BASIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="multiply_each", + expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, + doc={"arr": [1, 2, 3]}, + expected=[2, 4, 6], + msg="Should multiply each element by 2", + ), + ExpressionTestCase( + id="add_each", + expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 5]}}}, + doc={"arr": [10, 20, 30]}, + expected=[15, 25, 35], + msg="Should add 5 to each element", + ), + ExpressionTestCase( + id="identity", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": [1, 2, 3]}, + expected=[1, 2, 3], + msg="Identity transform should return same array", + ), + ExpressionTestCase( + id="constant_in", + expression={"$map": {"input": "$arr", "in": 13}}, + doc={"arr": [1, 2, 3]}, + expected=[13, 13, 13], + msg="Constant in expression should repeat for each element", + ), + ExpressionTestCase( + id="string_elements", + expression={"$map": {"input": "$arr", "in": {"$toUpper": "$$this"}}}, + doc={"arr": ["a", "b", "c"]}, + expected=["A", "B", "C"], + msg="Should uppercase each string element", + ), + ExpressionTestCase( + id="bool_elements", + expression={"$map": {"input": "$arr", "in": {"$not": "$$this"}}}, + doc={"arr": [True, False, True]}, + expected=[False, True, False], + msg="Should negate each boolean element", + ), + ExpressionTestCase( + id="single_element", + expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 1]}}}, + doc={"arr": [42]}, + expected=[43], + msg="Should map single element", + ), + ExpressionTestCase( + id="empty_array", + expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, + doc={"arr": []}, + expected=[], + msg="Should return empty array for empty input", + ), + ExpressionTestCase( + id="null_input", + expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, + doc={"arr": None}, + expected=None, + msg="Should return null when input is null", + ), + ExpressionTestCase( + id="custom_as_var", + expression={"$map": {"input": "$arr", "as": "val", "in": {"$multiply": ["$$val", 3]}}}, + doc={"arr": [1, 2, 3]}, + expected=[3, 6, 9], + msg="Should use custom 'as' variable name", + ), +] + +# --------------------------------------------------------------------------- +# Success: nested arrays (map does not flatten) +# --------------------------------------------------------------------------- +NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_arrays_identity", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": [[1, 2], [3, 4]]}, + expected=[[1, 2], [3, 4]], + msg="Should preserve nested arrays", + ), + ExpressionTestCase( + id="nested_arrays_size", + expression={"$map": {"input": "$arr", "in": {"$size": "$$this"}}}, + doc={"arr": [[1, 2], [3, 4, 5], []]}, + expected=[2, 3, 0], + msg="Should compute size of each subarray", + ), + ExpressionTestCase( + id="extract_field_from_objects", + expression={"$map": {"input": "$arr", "in": "$$this.a"}}, + doc={"arr": [{"a": 1, "b": 2}, {"a": 3, "b": 4}]}, + expected=[1, 3], + msg="Should extract field from each object element", + ), +] + +# --------------------------------------------------------------------------- +# Success: elements with null +# --------------------------------------------------------------------------- +NULL_ELEMENT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="null_elements_identity", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": [None, 1, None]}, + expected=[None, 1, None], + msg="Should preserve null elements", + ), + ExpressionTestCase( + id="null_elements_ifnull", + expression={"$map": {"input": "$arr", "in": {"$ifNull": ["$$this", 0]}}}, + doc={"arr": [None, 1, None]}, + expected=[0, 1, 0], + msg="Should replace null elements with $ifNull", + ), +] + +# --------------------------------------------------------------------------- +# Success: wrap each element +# --------------------------------------------------------------------------- +WRAP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="wrap_in_array", + expression={"$map": {"input": "$arr", "in": ["$$this"]}}, + doc={"arr": [1, 2, 3]}, + expected=[[1], [2], [3]], + msg="Should wrap each element in an array", + ), + ExpressionTestCase( + id="wrap_in_object", + expression={"$map": {"input": "$arr", "in": {"val": "$$this"}}}, + doc={"arr": [1, 2, 3]}, + expected=[{"val": 1}, {"val": 2}, {"val": 3}], + msg="Should wrap each element in an object", + ), +] + +# --------------------------------------------------------------------------- +# Success: type conversion in expression +# --------------------------------------------------------------------------- +TYPE_CONVERSION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="to_string", + expression={"$map": {"input": "$arr", "in": {"$toString": "$$this"}}}, + doc={"arr": [1, 2, 3]}, + expected=["1", "2", "3"], + msg="Should convert each element to string", + ), + ExpressionTestCase( + id="type_of_each", + expression={"$map": {"input": "$arr", "in": {"$type": "$$this"}}}, + doc={"arr": [1, "two", True, None]}, + expected=["int", "string", "bool", "null"], + msg="Should return type of each element", + ), +] + +# --------------------------------------------------------------------------- +# Success: large array +# --------------------------------------------------------------------------- +LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="large_array_1000", + expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, + doc={"arr": list(range(1000))}, + expected=[i * 2 for i in range(1000)], + msg="Should map over 1000 elements", + ), +] + +# --------------------------------------------------------------------------- +# Success: order preservation and duplicates +# --------------------------------------------------------------------------- +ORDER_DUPLICATE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="preserves_order", + expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 10]}}}, + doc={"arr": [3, 1, 4, 1, 5, 9]}, + expected=[30, 10, 40, 10, 50, 90], + msg="Should preserve element order", + ), + ExpressionTestCase( + id="duplicate_elements", + expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 10]}}}, + doc={"arr": [1, 1, 1, 1]}, + expected=[11, 11, 11, 11], + msg="Should process all duplicate elements", + ), + ExpressionTestCase( + id="duplicate_nulls", + expression={"$map": {"input": "$arr", "in": {"$ifNull": ["$$this", 0]}}}, + doc={"arr": [None, None, None]}, + expected=[0, 0, 0], + msg="Should process all null duplicates", + ), +] + + +# --------------------------------------------------------------------------- +# Success: null element propagation +# --------------------------------------------------------------------------- +NULL_PROPAGATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="null_add_propagation", + expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 1]}}}, + doc={"arr": [None, None]}, + expected=[None, None], + msg="Null + number should propagate null", + ), + ExpressionTestCase( + id="null_in_mixed_add", + expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 1]}}}, + doc={"arr": [1, 2, None, 4]}, + expected=[2, 3, None, 5], + msg="Null element should propagate, others should add", + ), + ExpressionTestCase( + id="null_input_ignores_in", + expression={"$map": {"input": "$arr", "in": {"$add": [2, 1]}}}, + doc={"arr": None}, + expected=None, + msg="Null input returns null regardless of in expression", + ), + ExpressionTestCase( + id="null_element_in_ignores", + expression={"$map": {"input": "$arr", "in": {"$add": [2, 1]}}}, + doc={"arr": [None]}, + expected=[3], + msg="Expression ignoring element should still produce result", + ), +] + +# --------------------------------------------------------------------------- +# Success: conditional in expressions +# --------------------------------------------------------------------------- +CONDITIONAL_IN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="cond_classify", + expression={ + "$map": {"input": "$arr", "in": {"$cond": [{"$gte": ["$$this", 3]}, "high", "low"]}} + }, + doc={"arr": [1, 2, 3, 4]}, + expected=["low", "low", "high", "high"], + msg="$cond should classify elements", + ), + ExpressionTestCase( + id="cond_isarray", + expression={ + "$map": { + "input": "$arr", + "in": {"$cond": [{"$isArray": "$$this"}, {"$size": "$$this"}, -1]}, + } + }, + doc={"arr": [[1, 2], "notArray", [3, 4, 5]]}, + expected=[2, -1, 3], + msg="$cond with $isArray should work", + ), +] + +# --------------------------------------------------------------------------- +# Success: complex nested in expressions +# --------------------------------------------------------------------------- +NESTED_IN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_multiply_add", + expression={"$map": {"input": "$arr", "in": {"$multiply": [{"$add": ["$$this", 1]}, 2]}}}, + doc={"arr": [1, 2, 3]}, + expected=[4, 6, 8], + msg="Nested expression should work", + ), + ExpressionTestCase( + id="self_reference_double", + expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", "$$this"]}}}, + doc={"arr": [1, 2, 3]}, + expected=[2, 4, 6], + msg="Self-reference should double each element", + ), + ExpressionTestCase( + id="produce_nested_arrays", + expression={"$map": {"input": "$arr", "in": ["$$this", {"$multiply": ["$$this", 2]}]}}, + doc={"arr": [1, 2, 3]}, + expected=[[1, 2], [2, 4], [3, 6]], + msg="Should produce nested arrays", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + BASIC_TESTS + + NESTED_ARRAY_TESTS + + NULL_ELEMENT_TESTS + + WRAP_TESTS + + TYPE_CONVERSION_TESTS + + LARGE_ARRAY_TESTS + + ORDER_DUPLICATE_TESTS + + NULL_PROPAGATION_TESTS + + CONDITIONAL_IN_TESTS + + NESTED_IN_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_map_insert(collection, test): + """Test $map with values from inserted documents.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_errors.py new file mode 100644 index 000000000..8ac8afb65 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_errors.py @@ -0,0 +1,287 @@ +""" +Error tests for $map expression. + +Tests non-array input (all BSON types, special numeric values, boundary values), +structural errors (missing fields, unknown fields). +Note: $map 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 ( # noqa: E501 + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + MAP_INPUT_NOT_ARRAY_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={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": "hello"}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject string input", + ), + ExpressionTestCase( + id="int_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": 42}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject int input", + ), + ExpressionTestCase( + id="negative_int_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": -42}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject negative int input", + ), + ExpressionTestCase( + id="bool_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": True}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject bool input", + ), + ExpressionTestCase( + id="object_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": {"a": 1}}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject object input", + ), + ExpressionTestCase( + id="double_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": 3.14}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject double input", + ), + ExpressionTestCase( + id="negative_double_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": -3.14}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject negative double input", + ), + ExpressionTestCase( + id="decimal128_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": Decimal128("1")}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject decimal128 input", + ), + ExpressionTestCase( + id="int64_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": Int64(1)}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject int64 input", + ), + ExpressionTestCase( + id="objectid_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": ObjectId()}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject objectid input", + ), + ExpressionTestCase( + id="datetime_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": datetime(2024, 1, 1)}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject datetime input", + ), + ExpressionTestCase( + id="binary_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": Binary(b"x", 0)}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject binary input", + ), + ExpressionTestCase( + id="regex_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": Regex("x")}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject regex input", + ), + ExpressionTestCase( + id="maxkey_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": MaxKey()}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject maxkey input", + ), + ExpressionTestCase( + id="minkey_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": MinKey()}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject minkey input", + ), + ExpressionTestCase( + id="timestamp_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": Timestamp(0, 0)}, + error_code=MAP_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={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": FLOAT_NAN}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject NaN input", + ), + ExpressionTestCase( + id="inf_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": FLOAT_INFINITY}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject Infinity input", + ), + ExpressionTestCase( + id="neg_inf_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": FLOAT_NEGATIVE_INFINITY}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject -Infinity input", + ), + ExpressionTestCase( + id="neg_zero_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": DOUBLE_NEGATIVE_ZERO}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject negative zero input", + ), + ExpressionTestCase( + id="decimal128_nan_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": DECIMAL128_NAN}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 NaN input", + ), + ExpressionTestCase( + id="decimal128_neg_nan_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": Decimal128("-NaN")}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 -NaN input", + ), + ExpressionTestCase( + id="decimal128_inf_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": DECIMAL128_INFINITY}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 Infinity input", + ), + ExpressionTestCase( + id="decimal128_neg_inf_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": DECIMAL128_NEGATIVE_INFINITY}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 -Infinity input", + ), + ExpressionTestCase( + id="decimal128_neg_zero_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": DECIMAL128_NEGATIVE_ZERO}, + error_code=MAP_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={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": INT32_MAX}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject INT32_MAX input", + ), + ExpressionTestCase( + id="int32_min_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": INT32_MIN}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject INT32_MIN input", + ), + ExpressionTestCase( + id="int64_max_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": INT64_MAX}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject INT64_MAX input", + ), + ExpressionTestCase( + id="int64_min_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": INT64_MIN}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject INT64_MIN input", + ), + ExpressionTestCase( + id="decimal128_max_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": DECIMAL128_MAX}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject DECIMAL128_MAX input", + ), + ExpressionTestCase( + id="decimal128_min_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": DECIMAL128_MIN}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="Should reject DECIMAL128_MIN input", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = NOT_ARRAY_ERROR_TESTS + SPECIAL_NUMERIC_ERROR_TESTS + BOUNDARY_ERROR_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_map_not_array_insert(collection, test): + """Test $map error with non-array input from inserted documents.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_expressions.py new file mode 100644 index 000000000..fc02d1be8 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_expressions.py @@ -0,0 +1,255 @@ +""" +Expression and field path tests for $map expression. + +Tests field path lookups, composite paths, system variables, +null/missing propagation via expressions, nested $map, and self-composition. +""" + +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 ( # noqa: E501 + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Field path lookups +# --------------------------------------------------------------------------- +FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_field_path", + expression={"$map": {"input": "$a.b", "in": {"$multiply": ["$$this", 2]}}}, + doc={"a": {"b": [1, 2, 3]}}, + expected=[2, 4, 6], + msg="Should resolve nested field path", + ), + ExpressionTestCase( + id="deeply_nested_field", + expression={"$map": {"input": "$a.b.c", "in": "$$this"}}, + doc={"a": {"b": {"c": [10, 20]}}}, + expected=[10, 20], + msg="Should resolve deeply nested field path", + ), + ExpressionTestCase( + id="composite_array_path", + expression={"$map": {"input": "$a.b", "in": {"$multiply": ["$$this", 10]}}}, + doc={"a": [{"b": 1}, {"b": 2}, {"b": 3}]}, + expected=[10, 20, 30], + msg="Composite array path should resolve to array", + ), + ExpressionTestCase( + id="index_path_on_object_key", + expression={"$map": {"input": "$a.0.b", "in": "$$this"}}, + doc={"a": {"0": {"b": [1, 2, 3]}}}, + expected=[1, 2, 3], + msg="Object key '0' resolves correctly", + ), + ExpressionTestCase( + id="object_key_zero", + expression={"$map": {"input": "$a.0", "in": "$$this"}}, + 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={"$map": {"input": "$arr", "in": {"$add": ["$$this", "$val"]}}}, + doc={"arr": [1, 2, 3], "val": 100}, + expected=[101, 102, 103], + msg="Should access outer document field in 'in' expression", + ), + ExpressionTestCase( + id="array_expression_input", + expression={"$map": {"input": ["$x", "$y"], "in": {"$multiply": ["$$this", 2]}}}, + doc={"x": 1, "y": 2}, + expected=[2, 4], + msg="Array expression with field refs resolved", + ), +] + +# --------------------------------------------------------------------------- +# $let and system variables +# --------------------------------------------------------------------------- +LET_AND_VARIABLE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="let_variable", + expression={ + "$let": { + "vars": {"arr": "$values"}, + "in": {"$map": {"input": "$$arr", "in": {"$add": ["$$this", 1]}}}, + } + }, + doc={"values": [1, 2, 3]}, + expected=[2, 3, 4], + msg="Should work with $let variables", + ), + ExpressionTestCase( + id="root_variable", + expression={"$map": {"input": "$$ROOT.values", "in": "$$this"}}, + doc={"_id": 1, "values": [10, 20]}, + expected=[10, 20], + msg="Should work with $$ROOT", + ), + ExpressionTestCase( + id="current_variable", + expression={"$map": {"input": "$$CURRENT.values", "in": "$$this"}}, + doc={"_id": 2, "values": [10, 20]}, + expected=[10, 20], + msg="$$CURRENT should be equivalent to field path", + ), +] + +# --------------------------------------------------------------------------- +# Null/missing via expression +# --------------------------------------------------------------------------- +NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="missing_field", + expression={"$map": {"input": "$nonexistent", "in": "$$this"}}, + doc={"other": 1}, + expected=None, + msg="Missing field should return null", + ), + ExpressionTestCase( + id="missing_input_type_is_null", + expression={"$type": {"$map": {"input": "$nonexistent", "in": "$$this"}}}, + doc={"x": 1}, + expected="null", + msg="Missing field should produce null type", + ), + ExpressionTestCase( + id="remove_variable", + expression={"$map": {"input": "$$REMOVE", "in": "$$this"}}, + doc={"x": 1}, + expected=None, + msg="$$REMOVE propagates null", + ), + ExpressionTestCase( + id="missing_field_in_expression", + expression={"$map": {"input": "$arr", "in": "$missing"}}, + doc={"arr": [1, 2, 3]}, + expected=[None, None, None], + msg="Missing field in 'in' should produce null for each element", + ), +] + +# --------------------------------------------------------------------------- +# Nested $map +# --------------------------------------------------------------------------- +NESTED_MAP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_map", + expression={ + "$map": { + "input": "$arr", + "as": "inarr", + "in": { + "$map": { + "input": "$$inarr", + "as": "num", + "in": {"$multiply": ["$$num", 2]}, + } + }, + } + }, + doc={"arr": [[1, 2], [3, 4]]}, + expected=[[2, 4], [6, 8]], + msg="Nested $map should process 2D array", + ), +] + + +# --------------------------------------------------------------------------- +# $map within $reduce and vice versa +# --------------------------------------------------------------------------- +REDUCE_INTERACTION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="map_within_reduce", + expression={ + "$reduce": { + "input": [4, 5, 6], + "initialValue": [0], + "in": { + "$concatArrays": ["$$value", {"$map": {"input": [1, 2, 3], "in": "$$this"}}] + }, + } + }, + doc={"_placeholder": 1}, + expected=[0, 1, 2, 3, 1, 2, 3, 1, 2, 3], + msg="$map's $$this should reference $map elements, not $reduce's", + ), + ExpressionTestCase( + id="reduce_within_map", + expression={ + "$map": { + "input": [100, 50], + "in": { + "$reduce": { + "input": [25, 25], + "initialValue": 1, + "in": {"$add": ["$$value", "$$this"]}, + } + }, + } + }, + doc={"_placeholder": 1}, + expected=[51, 51], + msg="$reduce's $$value and $$this should be scoped to $reduce", + ), +] + +# --------------------------------------------------------------------------- +# $$ROOT returning full document, $$REMOVE behavior +# --------------------------------------------------------------------------- +SYSTEM_VAR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="root_returns_full_doc", + expression={"$map": {"input": "$arr", "in": "$$ROOT"}}, + doc={"_id": 1, "arr": [1, 2]}, + expected=[{"_id": 1, "arr": [1, 2]}, {"_id": 1, "arr": [1, 2]}], + msg="$$ROOT should return root document for each element", + ), + ExpressionTestCase( + id="root_field_access", + expression={"$map": {"input": "$arr", "in": "$$ROOT.name"}}, + doc={"_id": 1, "arr": [1, 2], "name": "test"}, + expected=["test", "test"], + msg="$$ROOT.field should access root field for each element", + ), + ExpressionTestCase( + id="remove_in_cond_becomes_null", + expression={ + "$map": { + "input": [1, 10, 2, 20], + "in": {"$cond": [{"$gt": ["$$this", 5]}, "$$this", "$$REMOVE"]}, + } + }, + doc={"_placeholder": 1}, + expected=[None, 10, None, 20], + msg="$$REMOVE as array element becomes null, does not remove element", + ), +] + + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_EXPR_TESTS = ( + FIELD_LOOKUP_TESTS + + LET_AND_VARIABLE_TESTS + + NULL_MISSING_EXPR_TESTS + + NESTED_MAP_TESTS + + REDUCE_INTERACTION_TESTS + + SYSTEM_VAR_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_EXPR_TESTS)) +def test_map_expression(collection, test): + """Test $map with field paths and expressions.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_structure_errors.py new file mode 100644 index 000000000..8f854a1d7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_structure_errors.py @@ -0,0 +1,119 @@ +""" +Structural error tests for $map expression. + +Tests invalid $map argument structure: non-object argument, unknown/misspelled fields, +missing required fields (input, in), and runtime errors in the 'in' expression. +""" + +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 ( # noqa: E501 + execute_expression, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + EXPRESSION_NON_OBJECT_ARG_ERROR, + MAP_MISSING_IN_ERROR, + MAP_MISSING_INPUT_ERROR, + MAP_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={"$map": None}, + error_code=EXPRESSION_NON_OBJECT_ARG_ERROR, + msg="Null arg should error", + ), + ExpressionTestCase( + id="int_arg", + expression={"$map": 1}, + error_code=EXPRESSION_NON_OBJECT_ARG_ERROR, + msg="Int arg should error", + ), + ExpressionTestCase( + id="string_arg", + expression={"$map": "string"}, + error_code=EXPRESSION_NON_OBJECT_ARG_ERROR, + msg="String arg should error", + ), + ExpressionTestCase( + id="array_arg", + expression={"$map": [1, 2]}, + error_code=EXPRESSION_NON_OBJECT_ARG_ERROR, + msg="Array arg should error", + ), + ExpressionTestCase( + id="bool_arg", + expression={"$map": True}, + error_code=EXPRESSION_NON_OBJECT_ARG_ERROR, + msg="Bool arg should error", + ), +] + +# --------------------------------------------------------------------------- +# Error: unknown fields +# --------------------------------------------------------------------------- +UNKNOWN_FIELD_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="extra_unknown", + expression={"$map": {"input": [1], "in": "$$this", "unknown": 1}}, + error_code=MAP_UNKNOWN_FIELD_ERROR, + msg="Extra unknown field should error", + ), + ExpressionTestCase( + id="misspelled_inputs", + expression={"$map": {"inputs": [1], "in": "$$this"}}, + error_code=MAP_UNKNOWN_FIELD_ERROR, + msg="Misspelled 'inputs' should error", + ), +] + +# --------------------------------------------------------------------------- +# Error: missing required fields +# --------------------------------------------------------------------------- +MISSING_REQUIRED_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="missing_input", + expression={"$map": {"in": "$$this"}}, + error_code=MAP_MISSING_INPUT_ERROR, + msg="Missing input should error", + ), + ExpressionTestCase( + id="missing_in", + expression={"$map": {"input": [1, 2, 3]}}, + error_code=MAP_MISSING_IN_ERROR, + msg="Missing in should error", + ), + ExpressionTestCase( + id="missing_in_with_as", + expression={"$map": {"input": [1, 2, 3], "as": "x"}}, + error_code=MAP_MISSING_IN_ERROR, + msg="Missing in with as should error", + ), + ExpressionTestCase( + id="empty_object", + expression={"$map": {}}, + error_code=MAP_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_map_structure_error(collection, test): + """Test $map argument structure validation.""" + result = execute_expression(collection, test.expression) + assertResult(result, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_expression_map.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_expression_map.py index a5bf7ae58..080fdcc07 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_expression_map.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_expression_map.py @@ -38,4 +38,4 @@ def test_smoke_expression_map(collection): ) expected = [{"_id": 1, "doubled": [2, 4, 6]}, {"_id": 2, "doubled": [8, 10, 12]}] - assertSuccess(result, expected, msg="Should support $map expression") + assertSuccess(result, expected, "Should support $map expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_boundary.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_boundary.py new file mode 100644 index 000000000..4e009cd4a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_boundary.py @@ -0,0 +1,110 @@ +""" +Boundary value tests for $range expression. + +Tests INT32 boundary values and negative zero handling for start, end, step. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.range.utils.range_common import ( # noqa: E501 + RangeTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import INT32_MAX, INT32_MAX_MINUS_1, INT32_MIN + +# --------------------------------------------------------------------------- +# Success: INT32 boundary values +# --------------------------------------------------------------------------- +INT32_BOUNDARY_TESTS: list[RangeTest] = [ + RangeTest( + id="int32_max_eq", + start=INT32_MAX, + end=INT32_MAX, + expected=[], + msg="INT32_MAX == INT32_MAX should be empty", + ), + RangeTest( + id="int32_min_eq", + start=INT32_MIN, + end=INT32_MIN, + expected=[], + msg="INT32_MIN == INT32_MIN should be empty", + ), + RangeTest( + id="int32_max_minus1", + start=INT32_MAX_MINUS_1, + end=INT32_MAX, + expected=[INT32_MAX_MINUS_1], + msg="INT32_MAX-1 to INT32_MAX should produce single element", + ), + RangeTest( + id="int32_min_to_plus3", + start=INT32_MIN, + end=INT32_MIN + 3, + expected=[INT32_MIN, INT32_MIN + 1, INT32_MIN + 2], + msg="INT32_MIN to INT32_MIN+3", + ), + RangeTest( + id="step_int32_max", + start=0, + end=INT32_MAX, + step=INT32_MAX, + expected=[0], + msg="Step INT32_MAX should produce single element", + ), + RangeTest( + id="near_int32_max", + start=INT32_MAX - 7, + end=INT32_MAX, + expected=[ + INT32_MAX - 7, + INT32_MAX - 6, + INT32_MAX - 5, + INT32_MAX - 4, + INT32_MAX - 3, + INT32_MAX - 2, + INT32_MAX - 1, + ], + msg="Near INT32_MAX range", + ), + RangeTest( + id="cross_int32_boundary", + start=INT32_MIN + 1, + end=INT32_MAX, + step=INT32_MAX, + expected=[INT32_MIN + 1, 0], + msg="Cross INT32 boundary with large step", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_BOUNDARY_TESTS = INT32_BOUNDARY_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_BOUNDARY_TESTS)) +def test_range_boundary_insert(collection, test): + """Test $range boundary values with inserted documents.""" + doc = {"start": test.start, "end": test.end} + args = ["$start", "$end"] + if test.step is not None: + args.append("$step") + doc["step"] = test.step + result = execute_expression_with_insert(collection, {"$range": args}, doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + + +@pytest.mark.parametrize("test", pytest_params(ALL_BOUNDARY_TESTS)) +def test_range_boundary_literal(collection, test): + """Test $range boundary values with literal values.""" + args = [test.start, test.end] + if test.step is not None: + args.append(test.step) + result = execute_expression(collection, {"$range": args}) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_core_behavior.py new file mode 100644 index 000000000..049571761 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_core_behavior.py @@ -0,0 +1,496 @@ +""" +Core behavior tests for $range expression. + +Tests generating integer sequences with various start, end, step values, +negative ranges, empty results, and large ranges. +""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.range.utils.range_common import ( # noqa: E501 + RangeTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Success: basic ascending ranges (default step=1) +# --------------------------------------------------------------------------- +BASIC_ASC_TESTS: list[RangeTest] = [ + RangeTest( + id="zero_to_five", + start=0, + end=5, + expected=[0, 1, 2, 3, 4], + msg="Should generate 0..4", + ), + RangeTest( + id="one_to_four", + start=1, + end=4, + expected=[1, 2, 3], + msg="Should generate 1..3", + ), + RangeTest( + id="negative_range", + start=-5, + end=-1, + expected=[-5, -4, -3, -2], + msg="Should generate -5..-2", + ), + RangeTest( + id="start_equals_end", + start=5, + end=5, + expected=[], + msg="Should return empty when start equals end", + ), + RangeTest( + id="start_greater_than_end", + start=10, + end=5, + expected=[], + msg="Should return empty when start > end with default step", + ), +] + +# --------------------------------------------------------------------------- +# Success: custom step +# --------------------------------------------------------------------------- +STEP_TESTS: list[RangeTest] = [ + RangeTest( + id="step_two", + start=0, + end=10, + step=2, + expected=[0, 2, 4, 6, 8], + msg="Should generate with step 2", + ), + RangeTest( + id="step_three", + start=0, + end=10, + step=3, + expected=[0, 3, 6, 9], + msg="Should generate with step 3", + ), + RangeTest( + id="step_five", + start=0, + end=20, + step=5, + expected=[0, 5, 10, 15], + msg="Should generate with step 5", + ), + RangeTest( + id="step_one_explicit", + start=0, + end=3, + step=1, + expected=[0, 1, 2], + msg="Explicit step 1 same as default", + ), + RangeTest( + id="step_overshoots", + start=0, + end=5, + step=3, + expected=[0, 3], + msg="Should stop when step overshoots end", + ), + RangeTest( + id="step_exactly_reaches", + start=0, + end=6, + step=2, + expected=[0, 2, 4], + msg="End is exclusive even when step exactly reaches it", + ), + RangeTest( + id="start_nonzero", + start=5, + end=15, + expected=[5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + msg="Should work with nonzero start", + ), + RangeTest( + id="step_4", + start=5, + end=15, + step=4, + expected=[5, 9, 13], + msg="Should work with step 4", + ), +] + +# --------------------------------------------------------------------------- +# Success: negative step (descending) +# --------------------------------------------------------------------------- +NEGATIVE_STEP_TESTS: list[RangeTest] = [ + RangeTest( + id="descending_basic", + start=5, + end=0, + step=-1, + expected=[5, 4, 3, 2, 1], + msg="Should generate descending 5..1", + ), + RangeTest( + id="descending_step_two", + start=10, + end=0, + step=-2, + expected=[10, 8, 6, 4, 2], + msg="Should generate descending with step -2", + ), + RangeTest( + id="descending_negative_range", + start=-1, + end=-5, + step=-1, + expected=[-1, -2, -3, -4], + msg="Should generate descending in negative range", + ), + RangeTest( + id="descending_start_equals_end", + start=5, + end=5, + step=-1, + expected=[], + msg="Should return empty when start equals end with negative step", + ), + RangeTest( + id="descending_wrong_direction", + start=0, + end=5, + step=-1, + expected=[], + msg="Should return empty when step direction mismatches", + ), + RangeTest( + id="descending_step_neg3", + start=10, + end=0, + step=-3, + expected=[10, 7, 4, 1], + msg="Should generate descending with step -3", + ), + RangeTest( + id="descending_past_zero", + start=5, + end=-1, + step=-1, + expected=[5, 4, 3, 2, 1, 0], + msg="Should descend past zero", + ), +] + +# --------------------------------------------------------------------------- +# Success: empty results +# --------------------------------------------------------------------------- +EMPTY_TESTS: list[RangeTest] = [ + RangeTest( + id="ascending_wrong_direction", + start=5, + end=0, + step=1, + expected=[], + msg="Should return empty when ascending step but start > end", + ), + RangeTest( + id="zero_zero_pos_step", + start=0, + end=0, + step=1, + expected=[], + msg="0 to 0 step 1 should be empty", + ), + RangeTest( + id="zero_zero_neg_step", + start=0, + end=0, + step=-1, + expected=[], + msg="0 to 0 step -1 should be empty", + ), + RangeTest( + id="neg_equal", + start=-1, + end=-1, + expected=[], + msg="-1 to -1 should be empty", + ), + RangeTest( + id="large_equal", + start=1000000, + end=1000000, + expected=[], + msg="Large equal start/end should be empty", + ), + RangeTest( + id="neg_mismatch", + start=-1, + end=-5, + step=1, + expected=[], + msg="Negative range with positive step should be empty", + ), +] + +# --------------------------------------------------------------------------- +# Success: numeric type acceptance (int64, whole doubles, whole decimal128) +# --------------------------------------------------------------------------- +NUMERIC_TYPE_TESTS: list[RangeTest] = [ + RangeTest( + id="int64_args", + start=Int64(0), + end=Int64(3), + expected=[0, 1, 2], + msg="Should accept Int64 arguments", + ), + RangeTest( + id="whole_double_args", + start=0.0, + end=3.0, + expected=[0, 1, 2], + msg="Should accept whole-number double arguments", + ), + RangeTest( + id="whole_decimal128_args", + start=Decimal128("0"), + end=Decimal128("3"), + expected=[0, 1, 2], + msg="Should accept whole-number Decimal128 arguments", + ), + RangeTest( + id="int64_step", + start=0, + end=6, + step=Int64(2), + expected=[0, 2, 4], + msg="Should accept Int64 step", + ), + RangeTest( + id="whole_double_step", + start=0, + end=6, + step=2.0, + expected=[0, 2, 4], + msg="Should accept whole-number double step", + ), + RangeTest( + id="whole_decimal128_step", + start=0, + end=6, + step=Decimal128("2"), + expected=[0, 2, 4], + msg="Should accept whole-number Decimal128 step", + ), + RangeTest( + id="decimal128_trailing_zero_start", + start=Decimal128("0.0"), + end=3, + expected=[0, 1, 2], + msg="Should accept Decimal128 0.0 as start", + ), + RangeTest( + id="decimal128_trailing_zero_end", + start=0, + end=Decimal128("3.0"), + expected=[0, 1, 2], + msg="Should accept Decimal128 3.0 as end", + ), + RangeTest( + id="decimal128_trailing_zero_both", + start=Decimal128("0.0"), + end=Decimal128("3.0"), + expected=[0, 1, 2], + msg="Should accept Decimal128 0.0 start and 3.0 end", + ), +] + +# --------------------------------------------------------------------------- +# Success: single element result +# --------------------------------------------------------------------------- +SINGLE_ELEMENT_TESTS: list[RangeTest] = [ + RangeTest( + id="single_element", + start=0, + end=1, + expected=[0], + msg="Should return single element", + ), + RangeTest( + id="desc_single", + start=1, + end=0, + step=-1, + expected=[1], + msg="Descending single element", + ), + RangeTest( + id="step_eq_range", + start=0, + end=5, + step=5, + expected=[0], + msg="Step equal to range should produce single element", + ), + RangeTest( + id="step_gt_range", + start=0, + end=5, + step=10, + expected=[0], + msg="Step greater than range should produce single element", + ), +] + +# --------------------------------------------------------------------------- +# Success: negative number ranges +# --------------------------------------------------------------------------- +NEGATIVE_RANGE_TESTS: list[RangeTest] = [ + RangeTest( + id="neg_to_zero", + start=-5, + end=0, + expected=[-5, -4, -3, -2, -1], + msg="Negative start to zero ascending", + ), + RangeTest( + id="zero_to_neg", + start=0, + end=-5, + step=-1, + expected=[0, -1, -2, -3, -4], + msg="Zero to negative descending", + ), + RangeTest( + id="cross_zero_asc", + start=-3, + end=3, + expected=[-3, -2, -1, 0, 1, 2], + msg="Crossing zero ascending", + ), + RangeTest( + id="cross_zero_desc", + start=3, + end=-3, + step=-1, + expected=[3, 2, 1, 0, -1, -2], + msg="Crossing zero descending", + ), + RangeTest( + id="neg_desc_step2", + start=-10, + end=-20, + step=-2, + expected=[-10, -12, -14, -16, -18], + msg="Negative descending with step -2", + ), + RangeTest( + id="neg_asc_step2", + start=-10, + end=-1, + step=2, + expected=[-10, -8, -6, -4, -2], + msg="Negative ascending with step 2", + ), +] + +# --------------------------------------------------------------------------- +# Success: large range +# --------------------------------------------------------------------------- +LARGE_RANGE_TESTS: list[RangeTest] = [ + RangeTest( + id="large_range", + start=0, + end=1000, + expected=list(range(1000)), + msg="Should generate large range", + ), +] + +# --------------------------------------------------------------------------- +# Success: negative zero start/end +# --------------------------------------------------------------------------- +NEG_ZERO_TESTS: list[RangeTest] = [ + RangeTest( + id="neg_zero_double_start", + start=-0.0, + end=5, + expected=[0, 1, 2, 3, 4], + msg="Negative zero double start treated as 0", + ), + RangeTest( + id="neg_zero_decimal_start", + start=Decimal128("-0"), + end=5, + expected=[0, 1, 2, 3, 4], + msg="Negative zero Decimal128 start treated as 0", + ), + RangeTest( + id="neg_zero_double_end", + start=0, + end=-0.0, + expected=[], + msg="Negative zero double end treated as 0", + ), + RangeTest( + id="neg_zero_decimal_end", + start=0, + end=Decimal128("-0"), + expected=[], + msg="Negative zero Decimal128 end treated as 0", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + BASIC_ASC_TESTS + + STEP_TESTS + + NEGATIVE_STEP_TESTS + + EMPTY_TESTS + + NUMERIC_TYPE_TESTS + + SINGLE_ELEMENT_TESTS + + NEGATIVE_RANGE_TESTS + + LARGE_RANGE_TESTS + + NEG_ZERO_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_range_insert(collection, test): + """Test $range with values from inserted documents.""" + doc = {"start": test.start, "end": test.end} + args = ["$start", "$end"] + if test.step is not None: + args.append("$step") + doc["step"] = test.step + result = execute_expression_with_insert(collection, {"$range": args}, doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + + +TEST_SUBSET_FOR_LITERAL = [ + BASIC_ASC_TESTS[0], # zero_to_five + STEP_TESTS[0], # step_two + NEGATIVE_STEP_TESTS[0], # descending_basic + EMPTY_TESTS[0], # ascending_wrong_direction +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_range_literal(collection, test): + """Test $range with literal values.""" + args = [test.start, test.end] + if test.step is not None: + args.append(test.step) + result = execute_expression(collection, {"$range": args}) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_errors.py new file mode 100644 index 000000000..e75bfab98 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_errors.py @@ -0,0 +1,695 @@ +""" +Error tests for $range expression. + +Tests non-numeric types, non-integral values, step zero, out-of-range +int32 values, and wrong arity. + +""" + +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.range.utils.range_common import ( # noqa: E501 + RangeTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + EXPRESSION_ARITY_ERROR, + RANGE_END_NOT_INT32_ERROR, + RANGE_END_NOT_NUMERIC_ERROR, + RANGE_START_NOT_INT32_ERROR, + RANGE_START_NOT_INTEGRAL_ERROR, + RANGE_STEP_NOT_INT32_ERROR, + RANGE_STEP_NOT_NUMERIC_ERROR, + RANGE_STEP_ZERO_ERROR, +) +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, + INT32_OVERFLOW, + INT32_UNDERFLOW, + INT64_MAX, + INT64_MIN, +) + +# --------------------------------------------------------------------------- +# Error: non-numeric start +# --------------------------------------------------------------------------- +NON_NUMERIC_START_TESTS: list[RangeTest] = [ + RangeTest( + id="string_start", + start="hello", + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject string start", + ), + RangeTest( + id="bool_start", + start=True, + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject bool start", + ), + RangeTest( + id="null_start", + start=None, + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject null start", + ), + RangeTest( + id="object_start", + start={"a": 1}, + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject object start", + ), + RangeTest( + id="array_start", + start=[1], + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject array start", + ), + RangeTest( + id="objectid_start", + start=ObjectId(), + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject objectid start", + ), + RangeTest( + id="datetime_start", + start=datetime(2024, 1, 1), + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject datetime start", + ), + RangeTest( + id="binary_start", + start=Binary(b"x", 0), + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject binary start", + ), + RangeTest( + id="regex_start", + start=Regex("x"), + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject regex start", + ), + RangeTest( + id="maxkey_start", + start=MaxKey(), + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject maxkey start", + ), + RangeTest( + id="minkey_start", + start=MinKey(), + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject minkey start", + ), + RangeTest( + id="timestamp_start", + start=Timestamp(0, 0), + end=5, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject timestamp start", + ), +] + +# --------------------------------------------------------------------------- +# Error: non-numeric end +# --------------------------------------------------------------------------- +NON_NUMERIC_END_TESTS: list[RangeTest] = [ + RangeTest( + id="string_end", + start=0, + end="hello", + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject string end", + ), + RangeTest( + id="bool_end", + start=0, + end=True, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject bool end", + ), + RangeTest( + id="null_end", + start=0, + end=None, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject null end", + ), + RangeTest( + id="object_end", + start=0, + end={"a": 1}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject object end", + ), + RangeTest( + id="array_end", + start=0, + end=[1], + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject array end", + ), + RangeTest( + id="objectid_end", + start=0, + end=ObjectId(), + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject objectid end", + ), + RangeTest( + id="datetime_end", + start=0, + end=datetime(2024, 1, 1), + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject datetime end", + ), + RangeTest( + id="binary_end", + start=0, + end=Binary(b"x", 0), + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject binary end", + ), + RangeTest( + id="regex_end", + start=0, + end=Regex("x"), + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject regex end", + ), + RangeTest( + id="maxkey_end", + start=0, + end=MaxKey(), + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject maxkey end", + ), + RangeTest( + id="minkey_end", + start=0, + end=MinKey(), + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject minkey end", + ), + RangeTest( + id="timestamp_end", + start=0, + end=Timestamp(0, 0), + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject timestamp end", + ), +] + +# --------------------------------------------------------------------------- +# Error: non-numeric step +# --------------------------------------------------------------------------- +NON_NUMERIC_STEP_TESTS: list[RangeTest] = [ + RangeTest( + id="string_step", + start=0, + end=5, + step="bad", + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject string step", + ), + RangeTest( + id="bool_step", + start=0, + end=5, + step=True, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject bool step", + ), + RangeTest( + id="object_step", + start=0, + end=5, + step={"a": 1}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject object step", + ), + RangeTest( + id="array_step", + start=0, + end=5, + step=[1], + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject array step", + ), + RangeTest( + id="objectid_step", + start=0, + end=5, + step=ObjectId(), + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject objectid step", + ), + RangeTest( + id="datetime_step", + start=0, + end=5, + step=datetime(2024, 1, 1), + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject datetime step", + ), + RangeTest( + id="binary_step", + start=0, + end=5, + step=Binary(b"x", 0), + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject binary step", + ), + RangeTest( + id="regex_step", + start=0, + end=5, + step=Regex("x"), + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject regex step", + ), + RangeTest( + id="maxkey_step", + start=0, + end=5, + step=MaxKey(), + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject maxkey step", + ), + RangeTest( + id="minkey_step", + start=0, + end=5, + step=MinKey(), + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject minkey step", + ), + RangeTest( + id="timestamp_step", + start=0, + end=5, + step=Timestamp(0, 0), + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject timestamp step", + ), +] + +# --------------------------------------------------------------------------- +# Error: non-integral start +# --------------------------------------------------------------------------- +NON_INTEGRAL_START_TESTS: list[RangeTest] = [ + RangeTest( + id="fractional_start", + start=1.5, + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject fractional start", + ), + RangeTest( + id="decimal128_fractional_start", + start=Decimal128("0.5"), + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject fractional Decimal128 start", + ), + RangeTest( + id="negative_fractional_start", + start=-1.5, + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject negative fractional start", + ), + RangeTest( + id="decimal128_negative_fractional_start", + start=Decimal128("-1.5"), + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject negative fractional Decimal128 start", + ), + RangeTest( + id="decimal128_negative_nan_start", + start=Decimal128("-NaN"), + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject Decimal128 -NaN start", + ), +] + +# --------------------------------------------------------------------------- +# Error: non-integral end +# --------------------------------------------------------------------------- +NON_INTEGRAL_END_TESTS: list[RangeTest] = [ + RangeTest( + id="fractional_end", + start=0, + end=5.5, + error_code=RANGE_END_NOT_INT32_ERROR, + msg="Should reject fractional end", + ), + RangeTest( + id="decimal128_fractional_end", + start=0, + end=Decimal128("5.5"), + error_code=RANGE_END_NOT_INT32_ERROR, + msg="Should reject fractional Decimal128 end", + ), + RangeTest( + id="negative_fractional_end", + start=0, + end=-1.5, + error_code=RANGE_END_NOT_INT32_ERROR, + msg="Should reject negative fractional end", + ), + RangeTest( + id="decimal128_negative_fractional_end", + start=0, + end=Decimal128("-1.5"), + error_code=RANGE_END_NOT_INT32_ERROR, + msg="Should reject negative fractional Decimal128 end", + ), +] + +# --------------------------------------------------------------------------- +# Error: non-integral step +# --------------------------------------------------------------------------- +NON_INTEGRAL_STEP_TESTS: list[RangeTest] = [ + RangeTest( + id="fractional_step", + start=0, + end=10, + step=1.5, + error_code=RANGE_STEP_NOT_INT32_ERROR, + msg="Should reject fractional step", + ), + RangeTest( + id="decimal128_fractional_step", + start=0, + end=10, + step=Decimal128("1.5"), + error_code=RANGE_STEP_NOT_INT32_ERROR, + msg="Should reject fractional Decimal128 step", + ), + RangeTest( + id="negative_fractional_step", + start=10, + end=0, + step=-1.5, + error_code=RANGE_STEP_NOT_INT32_ERROR, + msg="Should reject negative fractional step", + ), + RangeTest( + id="decimal128_negative_fractional_step", + start=10, + end=0, + step=Decimal128("-1.5"), + error_code=RANGE_STEP_NOT_INT32_ERROR, + msg="Should reject negative fractional Decimal128 step", + ), +] + +# --------------------------------------------------------------------------- +# Error: special numeric values +# --------------------------------------------------------------------------- +SPECIAL_NUMERIC_TESTS: list[RangeTest] = [ + RangeTest( + id="nan_start", + start=FLOAT_NAN, + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject NaN start", + ), + RangeTest( + id="inf_start", + start=FLOAT_INFINITY, + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject Infinity start", + ), + RangeTest( + id="neg_inf_start", + start=FLOAT_NEGATIVE_INFINITY, + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject -Infinity start", + ), + RangeTest( + id="nan_end", + start=0, + end=FLOAT_NAN, + error_code=RANGE_END_NOT_INT32_ERROR, + msg="Should reject NaN end", + ), + RangeTest( + id="inf_end", + start=0, + end=FLOAT_INFINITY, + error_code=RANGE_END_NOT_INT32_ERROR, + msg="Should reject Infinity end", + ), + RangeTest( + id="decimal128_nan_start", + start=DECIMAL128_NAN, + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject Decimal128 NaN start", + ), + RangeTest( + id="decimal128_inf_start", + start=DECIMAL128_INFINITY, + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject Decimal128 Infinity start", + ), + RangeTest( + id="decimal128_neg_inf_end", + start=0, + end=DECIMAL128_NEGATIVE_INFINITY, + error_code=RANGE_END_NOT_INT32_ERROR, + msg="Should reject Decimal128 -Infinity end", + ), +] + +# --------------------------------------------------------------------------- +# Error: step zero → 34449 +# --------------------------------------------------------------------------- +STEP_ZERO_TESTS: list[RangeTest] = [ + RangeTest( + id="step_zero_int", + start=0, + end=5, + step=0, + error_code=RANGE_STEP_ZERO_ERROR, + msg="Should reject step 0", + ), + RangeTest( + id="step_zero_int64", + start=0, + end=5, + step=Int64(0), + error_code=RANGE_STEP_ZERO_ERROR, + msg="Should reject Int64 step 0", + ), + RangeTest( + id="step_zero_double", + start=0, + end=5, + step=0.0, + error_code=RANGE_STEP_ZERO_ERROR, + msg="Should reject double step 0.0", + ), + RangeTest( + id="step_zero_decimal128", + start=0, + end=5, + step=Decimal128("0"), + error_code=RANGE_STEP_ZERO_ERROR, + msg="Should reject Decimal128 step 0", + ), + RangeTest( + id="step_neg_zero_double", + start=0, + end=5, + step=-0.0, + error_code=RANGE_STEP_ZERO_ERROR, + msg="Should reject negative zero double step", + ), + RangeTest( + id="step_neg_zero_decimal128", + start=0, + end=5, + step=Decimal128("-0"), + error_code=RANGE_STEP_ZERO_ERROR, + msg="Should reject negative zero Decimal128 step", + ), +] + +# --------------------------------------------------------------------------- +# Error: out of int32 range +# --------------------------------------------------------------------------- +OUT_OF_INT32_TESTS: list[RangeTest] = [ + RangeTest( + id="start_int64_max", + start=INT64_MAX, + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject INT64_MAX start", + ), + RangeTest( + id="start_int64_min", + start=INT64_MIN, + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject INT64_MIN start", + ), + RangeTest( + id="end_int64_max", + start=0, + end=INT64_MAX, + error_code=RANGE_END_NOT_INT32_ERROR, + msg="Should reject INT64_MAX end", + ), + RangeTest( + id="end_int64_min", + start=0, + end=INT64_MIN, + error_code=RANGE_END_NOT_INT32_ERROR, + msg="Should reject INT64_MIN end", + ), + RangeTest( + id="step_int64_max", + start=0, + end=5, + step=INT64_MAX, + error_code=RANGE_STEP_NOT_INT32_ERROR, + msg="Should reject INT64_MAX step", + ), + RangeTest( + id="start_int32_overflow", + start=INT32_OVERFLOW, + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject INT32_OVERFLOW start", + ), + RangeTest( + id="start_int32_underflow", + start=INT32_UNDERFLOW, + end=5, + error_code=RANGE_START_NOT_INTEGRAL_ERROR, + msg="Should reject INT32_UNDERFLOW start", + ), + RangeTest( + id="end_int32_overflow", + start=0, + end=INT32_OVERFLOW, + error_code=RANGE_END_NOT_INT32_ERROR, + msg="Should reject INT32_OVERFLOW end", + ), + RangeTest( + id="end_int32_underflow", + start=0, + end=INT32_UNDERFLOW, + error_code=RANGE_END_NOT_INT32_ERROR, + msg="Should reject INT32_UNDERFLOW end", + ), + RangeTest( + id="step_int32_overflow", + start=0, + end=5, + step=INT32_OVERFLOW, + error_code=RANGE_STEP_NOT_INT32_ERROR, + msg="Should reject INT32_OVERFLOW step", + ), + RangeTest( + id="step_int32_underflow", + start=0, + end=5, + step=INT32_UNDERFLOW, + error_code=RANGE_STEP_NOT_INT32_ERROR, + msg="Should reject INT32_UNDERFLOW step", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + NON_NUMERIC_START_TESTS + + NON_NUMERIC_END_TESTS + + NON_NUMERIC_STEP_TESTS + + NON_INTEGRAL_START_TESTS + + NON_INTEGRAL_END_TESTS + + NON_INTEGRAL_STEP_TESTS + + SPECIAL_NUMERIC_TESTS + + STEP_ZERO_TESTS + + OUT_OF_INT32_TESTS +) + + +TEST_SUBSET_FOR_LITERAL = [ + NON_NUMERIC_START_TESTS[0], # string_start + NON_INTEGRAL_START_TESTS[0], # fractional_start + SPECIAL_NUMERIC_TESTS[0], # nan_start + STEP_ZERO_TESTS[0], # step_zero_int +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_range_error_literal(collection, test): + """Test $range error with literal values.""" + args = [test.start, test.end] + if test.step is not None: + args.append(test.step) + result = execute_expression(collection, {"$range": args}) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_range_error_insert(collection, test): + """Test $range error with values from inserted documents.""" + doc = {"start": test.start, "end": test.end} + args = ["$start", "$end"] + if test.step is not None: + args.append("$step") + doc["step"] = test.step + result = execute_expression_with_insert(collection, {"$range": args}, doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + + +# --------------------------------------------------------------------------- +# Error: wrong arity +# --------------------------------------------------------------------------- +ARITY_ERROR_TESTS = [ + pytest.param({"$range": []}, id="zero_args"), + pytest.param({"$range": [1]}, id="one_arg"), + pytest.param({"$range": [1, 2, 3, 4]}, id="four_args"), +] + + +@pytest.mark.parametrize("expr", ARITY_ERROR_TESTS) +def test_range_arity_error(collection, expr): + """Test $range errors with wrong number of arguments.""" + result = execute_expression(collection, expr) + assertResult(result, error_code=EXPRESSION_ARITY_ERROR) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_expressions.py new file mode 100644 index 000000000..de4e42042 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_expressions.py @@ -0,0 +1,169 @@ +""" +Expression and field path tests for $range expression. + +Tests field path lookups, composite paths, system variables, +and null/missing behavior. +""" + +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 ( # noqa: E501 + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + RANGE_END_NOT_NUMERIC_ERROR, + RANGE_START_NOT_INT32_ERROR, + RANGE_STEP_NOT_NUMERIC_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Field path lookups +# --------------------------------------------------------------------------- +FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_field_path", + expression={"$range": ["$a.start", "$a.end"]}, + doc={"a": {"start": 0, "end": 3}}, + expected=[0, 1, 2], + msg="Should resolve nested field paths", + ), + ExpressionTestCase( + id="deeply_nested_field", + expression={"$range": ["$a.b.start", "$a.b.end"]}, + doc={"a": {"b": {"start": 1, "end": 4}}}, + expected=[1, 2, 3], + msg="Should resolve deeply nested field paths", + ), + ExpressionTestCase( + id="step_from_field", + expression={"$range": ["$start", "$end", "$step"]}, + doc={"start": 0, "end": 10, "step": 3}, + expected=[0, 3, 6, 9], + msg="Should resolve step from field path", + ), + ExpressionTestCase( + id="nested_expr_start_end", + expression={"$range": [{"$add": [1, 2]}, {"$multiply": [2, 5]}]}, + doc={"_placeholder": 1}, + expected=[3, 4, 5, 6, 7, 8, 9], + msg="Should support nested expressions as start and end", + ), +] + +# --------------------------------------------------------------------------- +# $let and system variables +# --------------------------------------------------------------------------- +LET_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="let_variable", + expression={ + "$let": { + "vars": {"s": "$start", "e": "$end"}, + "in": {"$range": ["$$s", "$$e"]}, + } + }, + doc={"start": 0, "end": 3}, + expected=[0, 1, 2], + msg="Should work with $let variables", + ), + ExpressionTestCase( + id="root_variable", + expression={"$range": ["$$ROOT.start", "$$ROOT.end"]}, + doc={"_id": 1, "start": 0, "end": 3}, + expected=[0, 1, 2], + msg="Should work with $$ROOT", + ), + ExpressionTestCase( + id="current_variable", + expression={"$range": ["$$CURRENT.start", "$$CURRENT.end"]}, + doc={"_id": 2, "start": 0, "end": 3}, + expected=[0, 1, 2], + msg="$$CURRENT should be equivalent to field path", + ), + ExpressionTestCase( + id="remove_variable", + expression={"$range": ["$$REMOVE", 5]}, + doc={"x": 1}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="$$REMOVE should error like missing field", + ), +] + +# --------------------------------------------------------------------------- +# Null/missing via expression — $range does NOT propagate null, it errors +# --------------------------------------------------------------------------- +NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="missing_start_field", + expression={"$range": ["$nonexistent", 5]}, + doc={"other": 1}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Missing start field should error", + ), + ExpressionTestCase( + id="missing_end_field", + expression={"$range": [0, "$nonexistent"]}, + doc={"other": 1}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Missing end field should error", + ), + ExpressionTestCase( + id="null_start_field", + expression={"$range": ["$a", 5]}, + doc={"a": None}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Null start field should error", + ), + ExpressionTestCase( + id="null_end_field", + expression={"$range": [0, "$a"]}, + doc={"a": None}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Null end field should error", + ), + ExpressionTestCase( + id="null_step_field", + expression={"$range": [0, 5, "$a"]}, + doc={"a": None}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Null step field should error", + ), + ExpressionTestCase( + id="all_missing_fields", + expression={"$range": ["$a", "$b"]}, + doc={"_placeholder": 1}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="All missing should error on start first", + ), + ExpressionTestCase( + id="composite_array_path_error", + expression={"$range": ["$a.b", 5]}, + doc={"a": [{"b": 0}, {"b": 5}]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Composite array path should error", + ), + ExpressionTestCase( + id="array_index_path_error", + expression={"$range": ["$a.0", "$a.1", "$a.2"]}, + doc={"a": [0, 5, 2]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Array index path should error in expression context", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_EXPR_TESTS = FIELD_LOOKUP_TESTS + LET_TESTS + NULL_MISSING_EXPR_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_EXPR_TESTS)) +def test_range_expression(collection, test): + """Test $range with field paths and expressions.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_expression_range.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_expression_range.py index a425f73e3..0291009f7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_expression_range.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_expression_range.py @@ -26,4 +26,4 @@ def test_smoke_expression_range(collection): ) expected = [{"_id": 1, "sequence": [0, 1, 2, 3, 4]}, {"_id": 2, "sequence": [10, 11, 12]}] - assertSuccess(result, expected, msg="Should support $range expression") + assertSuccess(result, expected, "Should support $range expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/range_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/range_common.py new file mode 100644 index 000000000..43b2c2d5b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/range_common.py @@ -0,0 +1,17 @@ +""" +Shared test infrastructure for $range expression tests. +""" + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class RangeTest(BaseTestCase): + """Test case for $range operator.""" + + start: Any = None + end: Any = None + step: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_argument_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_argument_structure_errors.py new file mode 100644 index 000000000..78967ade8 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_argument_structure_errors.py @@ -0,0 +1,133 @@ +""" +Argument handling tests for $zip expression. + +Tests object structure validation: missing fields, extra fields, +non-object argument, empty inputs, inputs not being an array, +useLongestLength truthy/falsy coercion, and defaults type variety. +""" + +import pytest +from bson import MaxKey, MinKey + +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 ( # noqa: E501 + execute_expression, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + ZIP_INPUT_ELEMENT_NOT_ARRAY_ERROR, + ZIP_INPUTS_NOT_ARRAY_ERROR, + ZIP_MISSING_INPUTS_ERROR, + ZIP_UNKNOWN_FIELD_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +STRUCTURE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="empty_object", + expression={"$zip": {}}, + error_code=ZIP_MISSING_INPUTS_ERROR, + msg="Empty object should error", + ), + ExpressionTestCase( + id="missing_inputs", + expression={"$zip": {"useLongestLength": True}}, + error_code=ZIP_MISSING_INPUTS_ERROR, + msg="Missing inputs should error", + ), + ExpressionTestCase( + id="unknown_field", + expression={"$zip": {"inputs": [[1], [2]], "extra": 1}}, + error_code=ZIP_UNKNOWN_FIELD_ERROR, + msg="Unknown field should error", + ), + ExpressionTestCase( + id="inputs_not_array", + expression={"$zip": {"inputs": "bad"}}, + error_code=ZIP_INPUT_ELEMENT_NOT_ARRAY_ERROR, + msg="Non-array inputs should error", + ), + ExpressionTestCase( + id="inputs_not_array_int", + expression={"$zip": {"inputs": 1}}, + error_code=ZIP_INPUT_ELEMENT_NOT_ARRAY_ERROR, + msg="Int inputs should error", + ), + ExpressionTestCase( + id="inputs_empty_array", + expression={"$zip": {"inputs": []}}, + error_code=ZIP_MISSING_INPUTS_ERROR, + msg="Empty inputs array should error", + ), + ExpressionTestCase( + id="inputs_as_object", + expression={"$zip": {"inputs": {"a": "b"}}}, + error_code=ZIP_INPUT_ELEMENT_NOT_ARRAY_ERROR, + msg="inputs as object should error", + ), +] + +# --------------------------------------------------------------------------- +# Non-object argument (error 34460) +# --------------------------------------------------------------------------- +NON_OBJECT_ARG_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="int_arg", + expression={"$zip": 1}, + error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, + msg="Int argument should error", + ), + ExpressionTestCase( + id="string_arg", + expression={"$zip": "abc"}, + error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, + msg="String argument should error", + ), + ExpressionTestCase( + id="array_arg", + expression={"$zip": [1, 2]}, + error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, + msg="Array argument should error", + ), + ExpressionTestCase( + id="null_arg", + expression={"$zip": None}, + error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, + msg="Null argument should error", + ), + ExpressionTestCase( + id="bool_arg", + expression={"$zip": True}, + error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, + msg="Bool argument should error", + ), + ExpressionTestCase( + id="double_arg", + expression={"$zip": 1.5}, + error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, + msg="Double argument should error", + ), + ExpressionTestCase( + id="minkey_arg", + expression={"$zip": MinKey()}, + error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, + msg="MinKey argument should error", + ), + ExpressionTestCase( + id="maxkey_arg", + expression={"$zip": MaxKey()}, + error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, + msg="MaxKey argument should error", + ), +] + +ALL_STRUCTURE_TESTS = STRUCTURE_ERROR_TESTS + NON_OBJECT_ARG_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_STRUCTURE_TESTS)) +def test_zip_argument_handling(collection, test): + """Test $zip argument structure validation.""" + result = execute_expression(collection, test.expression) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_bson_types.py new file mode 100644 index 000000000..280f94f81 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_bson_types.py @@ -0,0 +1,331 @@ +""" +BSON type element preservation tests for $zip expression. + +Tests that various BSON types are preserved when zipping 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.zip.utils.zip_common import ( # noqa: E501 + ZipTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DOUBLE_NEGATIVE_ZERO, + FLOAT_INFINITY, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT64_MAX, + INT64_MIN, +) + +# --------------------------------------------------------------------------- +# BSON types preserved after zipping +# --------------------------------------------------------------------------- +BSON_TYPE_TESTS: list[ZipTest] = [ + ZipTest( + id="int64_values", + inputs=[[Int64(1)], [Int64(2)]], + expected=[[Int64(1), Int64(2)]], + msg="Should preserve Int64 values", + ), + ZipTest( + id="decimal128_values", + inputs=[[Decimal128("1.5")], [Decimal128("2.5")]], + expected=[[Decimal128("1.5"), Decimal128("2.5")]], + msg="Should preserve Decimal128 values", + ), + ZipTest( + id="datetime_values", + inputs=[[datetime(2024, 1, 1)], [datetime(2024, 6, 1)]], + expected=[[datetime(2024, 1, 1), datetime(2024, 6, 1)]], + msg="Should preserve datetime values", + ), + ZipTest( + id="objectid_values", + inputs=[ + [ObjectId("000000000000000000000001")], + [ObjectId("000000000000000000000002")], + ], + expected=[[ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")]], + msg="Should preserve ObjectId values", + ), + ZipTest( + id="binary_values", + inputs=[[Binary(b"\x01", 0)], [Binary(b"\x02", 0)]], + expected=[[b"\x01", b"\x02"]], + msg="Should preserve Binary values", + ), + ZipTest( + id="regex_values", + inputs=[[Regex("^a", "i")], [Regex("^b", "i")]], + expected=[[Regex("^a", "i"), Regex("^b", "i")]], + msg="Should preserve Regex values", + ), + ZipTest( + id="timestamp_values", + inputs=[[Timestamp(1, 0)], [Timestamp(2, 0)]], + expected=[[Timestamp(1, 0), Timestamp(2, 0)]], + msg="Should preserve Timestamp values", + ), + ZipTest( + id="minkey_maxkey", + inputs=[[MinKey()], [MaxKey()]], + expected=[[MinKey(), MaxKey()]], + msg="Should preserve MinKey/MaxKey values", + ), + ZipTest( + id="uuid_values", + inputs=[ + [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 across arrays +# --------------------------------------------------------------------------- +MIXED_BSON_TESTS: list[ZipTest] = [ + ZipTest( + id="mixed_bson_types", + inputs=[[1, "two", Int64(3)], [Decimal128("4"), True, MinKey()]], + expected=[[1, Decimal128("4")], ["two", True], [Int64(3), MinKey()]], + msg="Should zip mixed BSON types preserving each", + ), +] + +# --------------------------------------------------------------------------- +# Special numeric values as elements +# --------------------------------------------------------------------------- +SPECIAL_NUMERIC_TESTS: list[ZipTest] = [ + ZipTest( + id="infinity_values", + inputs=[[FLOAT_INFINITY], [FLOAT_NEGATIVE_INFINITY]], + expected=[[FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY]], + msg="Should preserve infinity values", + ), + ZipTest( + id="decimal128_infinity", + inputs=[[DECIMAL128_INFINITY], [DECIMAL128_NEGATIVE_INFINITY]], + expected=[[DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY]], + msg="Should preserve Decimal128 infinity values", + ), + ZipTest( + id="boundary_values", + inputs=[[INT32_MIN, INT32_MAX], [INT64_MIN, INT64_MAX]], + expected=[[INT32_MIN, INT64_MIN], [INT32_MAX, INT64_MAX]], + msg="Should preserve numeric boundary values", + ), + ZipTest( + id="negative_zero", + inputs=[[DOUBLE_NEGATIVE_ZERO], [0]], + expected=[[DOUBLE_NEGATIVE_ZERO, 0]], + msg="Should preserve negative zero", + ), + ZipTest( + id="decimal128_nan", + inputs=[[DECIMAL128_NAN], [Decimal128("1")]], + expected=[[DECIMAL128_NAN, Decimal128("1")]], + msg="Should preserve Decimal128 NaN", + ), +] + +# --------------------------------------------------------------------------- +# Defaults with BSON types +# --------------------------------------------------------------------------- +BSON_DEFAULTS_TESTS: list[ZipTest] = [ + ZipTest( + id="default_int64", + inputs=[[1, 2, 3], [Int64(10)]], + use_longest_length=True, + defaults=[0, Int64(0)], + expected=[[1, Int64(10)], [2, Int64(0)], [3, Int64(0)]], + msg="Should use Int64 default value", + ), + ZipTest( + id="default_decimal128", + inputs=[[1, 2, 3], [Decimal128("1.5")]], + use_longest_length=True, + defaults=[0, Decimal128("0")], + expected=[[1, Decimal128("1.5")], [2, Decimal128("0")], [3, Decimal128("0")]], + msg="Should use Decimal128 default value", + ), + ZipTest( + id="default_datetime", + inputs=[[1, 2], [datetime(2024, 1, 1)]], + use_longest_length=True, + defaults=[0, datetime(1970, 1, 1)], + expected=[[1, datetime(2024, 1, 1)], [2, datetime(1970, 1, 1)]], + msg="Should use datetime default value", + ), + ZipTest( + id="default_objectid", + inputs=[[1, 2], [ObjectId("000000000000000000000001")]], + use_longest_length=True, + defaults=[0, ObjectId("000000000000000000000000")], + expected=[ + [1, ObjectId("000000000000000000000001")], + [2, ObjectId("000000000000000000000000")], + ], + msg="Should use ObjectId default value", + ), + ZipTest( + id="default_timestamp", + inputs=[[1, 2], [Timestamp(1, 0)]], + use_longest_length=True, + defaults=[0, Timestamp(0, 0)], + expected=[[1, Timestamp(1, 0)], [2, Timestamp(0, 0)]], + msg="Should use Timestamp default value", + ), + ZipTest( + id="default_regex", + inputs=[[1, 2], [Regex("^a", "i")]], + use_longest_length=True, + defaults=[0, Regex(".*", "")], + expected=[[1, Regex("^a", "i")], [2, Regex(".*", "")]], + msg="Should use Regex default value", + ), + ZipTest( + id="default_minkey_maxkey", + inputs=[[1, 2], [MinKey()]], + use_longest_length=True, + defaults=[0, MaxKey()], + expected=[[1, MinKey()], [2, MaxKey()]], + msg="Should use MaxKey default value", + ), + ZipTest( + id="default_binary", + inputs=[[1, 2], [Binary(b"\x01", 0)]], + use_longest_length=True, + defaults=[0, Binary(b"\x00", 0)], + expected=[[1, b"\x01"], [2, b"\x00"]], + msg="Should use Binary default value", + ), +] + +# --------------------------------------------------------------------------- +# Defaults with special numeric values +# --------------------------------------------------------------------------- +SPECIAL_NUMERIC_DEFAULTS_TESTS: list[ZipTest] = [ + ZipTest( + id="default_infinity", + inputs=[[1, 2], [FLOAT_INFINITY]], + use_longest_length=True, + defaults=[0, FLOAT_INFINITY], + expected=[[1, FLOAT_INFINITY], [2, FLOAT_INFINITY]], + msg="Should use infinity as default", + ), + ZipTest( + id="default_negative_infinity", + inputs=[[1, 2], [FLOAT_NEGATIVE_INFINITY]], + use_longest_length=True, + defaults=[0, FLOAT_NEGATIVE_INFINITY], + expected=[[1, FLOAT_NEGATIVE_INFINITY], [2, FLOAT_NEGATIVE_INFINITY]], + msg="Should use negative infinity as default", + ), + ZipTest( + id="default_negative_zero", + inputs=[[1, 2], [DOUBLE_NEGATIVE_ZERO]], + use_longest_length=True, + defaults=[0, DOUBLE_NEGATIVE_ZERO], + expected=[[1, DOUBLE_NEGATIVE_ZERO], [2, DOUBLE_NEGATIVE_ZERO]], + msg="Should use negative zero as default", + ), + ZipTest( + id="default_int32_boundaries", + inputs=[[1, 2], [INT32_MIN]], + use_longest_length=True, + defaults=[0, INT32_MAX], + expected=[[1, INT32_MIN], [2, INT32_MAX]], + msg="Should use INT32_MAX as default", + ), + ZipTest( + id="default_int64_boundaries", + inputs=[[1, 2], [INT64_MIN]], + use_longest_length=True, + defaults=[0, INT64_MAX], + expected=[[1, INT64_MIN], [2, INT64_MAX]], + msg="Should use INT64_MAX as default", + ), + ZipTest( + id="default_decimal128_infinity", + inputs=[[1, 2], [DECIMAL128_INFINITY]], + use_longest_length=True, + defaults=[0, DECIMAL128_NEGATIVE_INFINITY], + expected=[[1, DECIMAL128_INFINITY], [2, DECIMAL128_NEGATIVE_INFINITY]], + msg="Should use Decimal128 negative infinity as default", + ), + ZipTest( + id="default_decimal128_nan", + inputs=[[1, 2], [DECIMAL128_NAN]], + use_longest_length=True, + defaults=[0, DECIMAL128_NAN], + expected=[[1, DECIMAL128_NAN], [2, DECIMAL128_NAN]], + msg="Should use Decimal128 NaN as default", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_BSON_TESTS = ( + BSON_TYPE_TESTS + + MIXED_BSON_TESTS + + SPECIAL_NUMERIC_TESTS + + BSON_DEFAULTS_TESTS + + SPECIAL_NUMERIC_DEFAULTS_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_BSON_TESTS)) +def test_zip_bson_insert(collection, test): + """Test $zip BSON types with values from inserted documents.""" + expr = {"inputs": [f"$arr{i}" for i in range(len(test.inputs))]} + if test.use_longest_length is not None: + expr["useLongestLength"] = test.use_longest_length + if test.defaults is not None: + expr["defaults"] = test.defaults + doc = {f"arr{i}": arr for i, arr in enumerate(test.inputs)} + result = execute_expression_with_insert(collection, {"$zip": expr}, doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + + +TEST_SUBSET_FOR_LITERAL = [ + BSON_TYPE_TESTS[0], # int64_values + BSON_TYPE_TESTS[4], # binary_values + MIXED_BSON_TESTS[0], # mixed_bson_types + SPECIAL_NUMERIC_TESTS[0], # infinity_values +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_zip_bson_literal(collection, test): + """Test $zip BSON types with literal values.""" + expr = {"inputs": [{"$literal": a} for a in test.inputs]} + if test.use_longest_length is not None: + expr["useLongestLength"] = test.use_longest_length + if test.defaults is not None: + expr["defaults"] = test.defaults + result = execute_expression(collection, {"$zip": expr}) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_core_behavior.py new file mode 100644 index 000000000..f3297bf2c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_core_behavior.py @@ -0,0 +1,442 @@ +""" +Core behavior tests for $zip expression. + +Tests zipping arrays of various element types, equal/unequal lengths, +useLongestLength, defaults, empty arrays, single arrays, nested arrays, +null propagation, and large arrays. +""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.zip.utils.zip_common import ( # noqa: E501 + ZipTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Success: basic zipping — equal length arrays +# --------------------------------------------------------------------------- +BASIC_TESTS: list[ZipTest] = [ + ZipTest( + id="two_int_arrays", + inputs=[[1, 2, 3], [10, 20, 30]], + expected=[[1, 10], [2, 20], [3, 30]], + msg="Should zip two int arrays", + ), + ZipTest( + id="two_string_arrays", + inputs=[["a", "b"], ["c", "d"]], + expected=[["a", "c"], ["b", "d"]], + msg="Should zip two string arrays", + ), + ZipTest( + id="three_arrays", + inputs=[[1, 2], [10, 20], [100, 200]], + expected=[[1, 10, 100], [2, 20, 200]], + msg="Should zip three arrays", + ), + ZipTest( + id="mixed_type_elements", + inputs=[[1, "two"], [True, None]], + expected=[[1, True], ["two", None]], + msg="Should zip arrays with mixed types", + ), + ZipTest( + id="numeric_cross_types", + inputs=[[1, Int64(2)], [3.0, Decimal128("4")]], + expected=[[1, 3.0], [Int64(2), Decimal128("4")]], + msg="Should zip mixed numeric type arrays", + ), +] + +# --------------------------------------------------------------------------- +# Success: unequal length — truncate to shortest (default) +# --------------------------------------------------------------------------- +UNEQUAL_LENGTH_TESTS: list[ZipTest] = [ + ZipTest( + id="first_shorter", + inputs=[[1, 2], [10, 20, 30]], + expected=[[1, 10], [2, 20]], + msg="Should truncate to shorter first array", + ), + ZipTest( + id="second_shorter", + inputs=[[1, 2, 3], [10, 20]], + expected=[[1, 10], [2, 20]], + msg="Should truncate to shorter second array", + ), + ZipTest( + id="one_empty", + inputs=[[], [1, 2, 3]], + expected=[], + msg="Empty array should produce empty result", + ), +] + +# --------------------------------------------------------------------------- +# Success: useLongestLength +# --------------------------------------------------------------------------- +USE_LONGEST_TESTS: list[ZipTest] = [ + ZipTest( + id="longest_pads_null", + inputs=[[1, 2, 3], [10, 20]], + use_longest_length=True, + expected=[[1, 10], [2, 20], [3, None]], + msg="Should pad shorter array with null", + ), + ZipTest( + id="longest_both_short", + inputs=[[1], [10, 20], [100, 200, 300]], + use_longest_length=True, + expected=[[1, 10, 100], [None, 20, 200], [None, None, 300]], + msg="Should pad multiple shorter arrays with null", + ), + ZipTest( + id="longest_equal_length", + inputs=[[1, 2], [10, 20]], + use_longest_length=True, + expected=[[1, 10], [2, 20]], + msg="Equal length with useLongestLength should behave same", + ), + ZipTest( + id="longest_false_truncates", + inputs=[[1, 2, 3], [10, 20]], + use_longest_length=False, + expected=[[1, 10], [2, 20]], + msg="useLongestLength false should truncate", + ), +] + +# --------------------------------------------------------------------------- +# Success: defaults +# --------------------------------------------------------------------------- +DEFAULTS_TESTS: list[ZipTest] = [ + ZipTest( + id="defaults_fill_shorter", + inputs=[[1, 2, 3], [10, 20]], + use_longest_length=True, + defaults=[0, 0], + expected=[[1, 10], [2, 20], [3, 0]], + msg="Should fill shorter array with default value", + ), + ZipTest( + id="defaults_multiple_arrays", + inputs=[[1], [10, 20], [100, 200, 300]], + use_longest_length=True, + defaults=[-1, -2, -3], + expected=[[1, 10, 100], [-1, 20, 200], [-1, -2, 300]], + msg="Should fill multiple shorter arrays with respective defaults", + ), + ZipTest( + id="defaults_null_value", + inputs=[[1, 2, 3], [10]], + use_longest_length=True, + defaults=[None, None], + expected=[[1, 10], [2, None], [3, None]], + msg="Null defaults should work same as no defaults", + ), + ZipTest( + id="defaults_string", + inputs=[[1, 2, 3], ["a"]], + use_longest_length=True, + defaults=[0, "missing"], + expected=[[1, "a"], [2, "missing"], [3, "missing"]], + msg="Should use string default value", + ), + ZipTest( + id="defaults_not_used_equal_length", + inputs=[[1, 2], [10, 20]], + use_longest_length=True, + defaults=[0, 0], + expected=[[1, 10], [2, 20]], + msg="Defaults not used when arrays are equal length", + ), + ZipTest( + id="defaults_falsy_empty_string", + inputs=[[1, 2], ["a"]], + use_longest_length=True, + defaults=[0, ""], + expected=[[1, "a"], [2, ""]], + msg="Falsy defaults (empty string) used correctly", + ), + ZipTest( + id="defaults_false", + inputs=[[1, 2], ["a"]], + use_longest_length=True, + defaults=[0, False], + expected=[[1, "a"], [2, False]], + msg="False default used correctly", + ), + ZipTest( + id="defaults_complex_types", + inputs=[[1, 2], ["a"]], + use_longest_length=True, + defaults=[{"x": 1}, [1, 2]], + expected=[[1, "a"], [2, [1, 2]]], + msg="Complex type defaults used correctly", + ), + ZipTest( + id="defaults_nested_array", + inputs=[[1, 2], ["a"]], + use_longest_length=True, + defaults=[[1, 2], "fallback"], + expected=[[1, "a"], [2, "fallback"]], + msg="Nested array default used as-is", + ), +] + +# --------------------------------------------------------------------------- +# Success: empty and single element +# --------------------------------------------------------------------------- +DEGENERATE_TESTS: list[ZipTest] = [ + ZipTest( + id="both_empty", + inputs=[[], []], + expected=[], + msg="Should return empty for two empty arrays", + ), + ZipTest( + id="three_empty", + inputs=[[], [], []], + expected=[], + msg="Three empty arrays return []", + ), + ZipTest( + id="single_empty", + inputs=[[]], + expected=[], + msg="Single empty array returns []", + ), + ZipTest( + id="single_element_each", + inputs=[[1], [10]], + expected=[[1, 10]], + msg="Should zip single-element arrays", + ), + ZipTest( + id="single_input_array", + inputs=[[1, 2, 3]], + expected=[[1], [2], [3]], + msg="Single input should wrap each element", + ), + ZipTest( + id="all_single_element_three", + inputs=[[1], ["a"], [True]], + expected=[[1, "a", True]], + msg="All single-element arrays produce one row", + ), + ZipTest( + id="empty_with_longest_false", + inputs=[[], [1, 2, 3]], + expected=[], + msg="Empty array with shortest length returns []", + ), + ZipTest( + id="empty_with_longest_true", + inputs=[[], [1, 2, 3]], + use_longest_length=True, + expected=[[None, 1], [None, 2], [None, 3]], + msg="Empty array with longest length pads with null", + ), + ZipTest( + id="two_empty_longest_true", + inputs=[[], []], + use_longest_length=True, + expected=[], + msg="Two empty arrays with longest length return []", + ), +] + +# --------------------------------------------------------------------------- +# Success: nested arrays as elements +# --------------------------------------------------------------------------- +NESTED_ARRAY_TESTS: list[ZipTest] = [ + ZipTest( + id="nested_arrays", + inputs=[[[1, 2], [3, 4]], ["a", "b"]], + expected=[[[1, 2], "a"], [[3, 4], "b"]], + msg="Should zip nested arrays as elements", + ), + ZipTest( + id="objects_as_elements", + inputs=[[{"x": 1}, {"x": 2}], [{"y": 10}, {"y": 20}]], + expected=[[{"x": 1}, {"y": 10}], [{"x": 2}, {"y": 20}]], + msg="Objects preserved as elements", + ), + ZipTest( + id="mixed_types_six", + inputs=[[1, "a", None, True, {"k": 1}, [9]], [10, 20, 30, 40, 50, 60]], + expected=[[1, 10], ["a", 20], [None, 30], [True, 40], [{"k": 1}, 50], [[9], 60]], + msg="Mixed types preserved in transposition", + ), +] + +# --------------------------------------------------------------------------- +# Success: null propagation +# --------------------------------------------------------------------------- +NULL_TESTS: list[ZipTest] = [ + ZipTest( + id="null_first_input", + inputs=[None, [1, 2]], + expected=None, + msg="Should return null when first input is null", + ), + ZipTest( + id="null_second_input", + inputs=[[1, 2], None], + expected=None, + msg="Should return null when second input is null", + ), + ZipTest( + id="all_null_inputs", + inputs=[None, None], + expected=None, + msg="Should return null when all inputs are null", + ), + ZipTest( + id="null_elements_in_arrays", + inputs=[[1, None], [None, 2]], + expected=[[1, None], [None, 2]], + msg="Should preserve null elements within arrays", + ), +] + +# --------------------------------------------------------------------------- +# Success: objects as elements +# --------------------------------------------------------------------------- +OBJECT_TESTS: list[ZipTest] = [ + ZipTest( + id="arrays_of_objects", + inputs=[[{"a": 1}], [{"b": 2}]], + expected=[[{"a": 1}, {"b": 2}]], + msg="Should zip arrays of objects", + ), +] + +# --------------------------------------------------------------------------- +# Success: large arrays +# --------------------------------------------------------------------------- +LARGE_ARRAY_TESTS: list[ZipTest] = [ + ZipTest( + id="large_arrays", + inputs=[list(range(1000)), list(range(1000, 2000))], + expected=[[i, i + 1000] for i in range(1000)], + msg="Should zip large arrays", + ), +] + +# --------------------------------------------------------------------------- +# Success: many input arrays +# --------------------------------------------------------------------------- +MANY_INPUTS_TESTS: list[ZipTest] = [ + ZipTest( + id="ten_inputs", + inputs=[[i] for i in range(10)], + expected=[list(range(10))], + msg="Ten inputs transpose correctly", + ), + ZipTest( + id="fifty_inputs", + inputs=[[i] for i in range(50)], + expected=[list(range(50))], + msg="50 single-element inputs produce one 50-element row", + ), +] + +# --------------------------------------------------------------------------- +# Success: multiple arrays of different lengths +# --------------------------------------------------------------------------- +MULTI_LENGTH_TESTS: list[ZipTest] = [ + ZipTest( + id="three_arrays_shortest", + inputs=[[1], [10, 20, 30], [100, 200, 300, 400, 500]], + expected=[[1, 10, 100]], + msg="Three arrays shortest = 1", + ), + ZipTest( + id="three_arrays_longest_no_defaults", + inputs=[[1], [10, 20, 30], [100, 200, 300, 400, 500]], + use_longest_length=True, + expected=[ + [1, 10, 100], + [None, 20, 200], + [None, 30, 300], + [None, None, 400], + [None, None, 500], + ], + msg="Three arrays longest pads with null", + ), + ZipTest( + id="three_arrays_longest_with_defaults", + inputs=[[1], [10, 20, 30], [100, 200, 300, 400, 500]], + use_longest_length=True, + defaults=[0, "x", False], + expected=[[1, 10, 100], [0, 20, 200], [0, 30, 300], [0, "x", 400], [0, "x", 500]], + msg="Three arrays longest with defaults", + ), + ZipTest( + id="four_arrays_two_empty", + inputs=[[], [], [1, 2], [3, 4, 5]], + use_longest_length=True, + expected=[[None, None, 1, 3], [None, None, 2, 4], [None, None, None, 5]], + msg="Four arrays two empty with longest", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + BASIC_TESTS + + UNEQUAL_LENGTH_TESTS + + USE_LONGEST_TESTS + + DEFAULTS_TESTS + + DEGENERATE_TESTS + + NESTED_ARRAY_TESTS + + NULL_TESTS + + OBJECT_TESTS + + LARGE_ARRAY_TESTS + + MANY_INPUTS_TESTS + + MULTI_LENGTH_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_zip_insert(collection, test): + """Test $zip with values from inserted documents.""" + expr = {} + expr["inputs"] = [f"$arr{i}" for i in range(len(test.inputs))] + if test.use_longest_length is not None: + expr["useLongestLength"] = test.use_longest_length + if test.defaults is not None: + expr["defaults"] = test.defaults + doc = {f"arr{i}": arr for i, arr in enumerate(test.inputs)} + result = execute_expression_with_insert(collection, {"$zip": expr}, doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + + +TEST_SUBSET_FOR_LITERAL = [ + BASIC_TESTS[0], # two_int_arrays + BASIC_TESTS[2], # three_arrays + UNEQUAL_LENGTH_TESTS[0], # first_shorter + DEGENERATE_TESTS[0], # both_empty +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_zip_literal(collection, test): + """Test $zip with literal values.""" + expr = {} + expr["inputs"] = [{"$literal": a} for a in test.inputs] + if test.use_longest_length is not None: + expr["useLongestLength"] = test.use_longest_length + if test.defaults is not None: + expr["defaults"] = test.defaults + result = execute_expression(collection, {"$zip": expr}) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_errors.py new file mode 100644 index 000000000..b745f5d7a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_errors.py @@ -0,0 +1,440 @@ +""" +Error tests for $zip expression. + +Tests non-array inputs, invalid useLongestLength, invalid defaults, +defaults without useLongestLength, and defaults length mismatch. +""" + +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.zip.utils.zip_common import ( # noqa: E501 + ZipTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + ZIP_DEFAULTS_LENGTH_MISMATCH_ERROR, + ZIP_DEFAULTS_NOT_ARRAY_ERROR, + ZIP_DEFAULTS_WITHOUT_LONGEST_ERROR, + ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + ZIP_USE_LONGEST_NOT_BOOL_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 element — standard BSON types +# --------------------------------------------------------------------------- +NOT_ARRAY_ELEMENT_TESTS: list[ZipTest] = [ + ZipTest( + id="string_input", + inputs=["hello", [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject string input element", + ), + ZipTest( + id="int_input", + inputs=[42, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject int input element", + ), + ZipTest( + id="negative_int_input", + inputs=[-42, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject negative int input element", + ), + ZipTest( + id="bool_input", + inputs=[True, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject bool input element", + ), + ZipTest( + id="object_input", + inputs=[{"a": 1}, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject object input element", + ), + ZipTest( + id="double_input", + inputs=[3.14, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject double input element", + ), + ZipTest( + id="negative_double_input", + inputs=[-3.14, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject negative double input element", + ), + ZipTest( + id="decimal128_input", + inputs=[Decimal128("1"), [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject decimal128 input element", + ), + ZipTest( + id="int64_input", + inputs=[Int64(1), [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject int64 input element", + ), + ZipTest( + id="objectid_input", + inputs=[ObjectId(), [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject objectid input element", + ), + ZipTest( + id="datetime_input", + inputs=[datetime(2024, 1, 1), [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject datetime input element", + ), + ZipTest( + id="binary_input", + inputs=[Binary(b"x", 0), [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject binary input element", + ), + ZipTest( + id="regex_input", + inputs=[Regex("x"), [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject regex input element", + ), + ZipTest( + id="maxkey_input", + inputs=[MaxKey(), [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject maxkey input element", + ), + ZipTest( + id="minkey_input", + inputs=[MinKey(), [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject minkey input element", + ), + ZipTest( + id="timestamp_input", + inputs=[Timestamp(0, 0), [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject timestamp input element", + ), + ZipTest( + id="non_array_second_position", + inputs=[[1], 42], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject non-array in second position", + ), + ZipTest( + id="non_array_middle_position", + inputs=[[1], "bad", [2]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject non-array in middle position", + ), +] + +# --------------------------------------------------------------------------- +# Error: special float/Decimal128 values as input element +# --------------------------------------------------------------------------- +SPECIAL_NUMERIC_ERROR_TESTS: list[ZipTest] = [ + ZipTest( + id="nan_input", + inputs=[FLOAT_NAN, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject NaN input element", + ), + ZipTest( + id="inf_input", + inputs=[FLOAT_INFINITY, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject Infinity input element", + ), + ZipTest( + id="neg_inf_input", + inputs=[FLOAT_NEGATIVE_INFINITY, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject -Infinity input element", + ), + ZipTest( + id="neg_zero_input", + inputs=[DOUBLE_NEGATIVE_ZERO, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject negative zero input element", + ), + ZipTest( + id="decimal128_nan_input", + inputs=[DECIMAL128_NAN, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject Decimal128 NaN input element", + ), + ZipTest( + id="decimal128_inf_input", + inputs=[DECIMAL128_INFINITY, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject Decimal128 Infinity input element", + ), + ZipTest( + id="decimal128_neg_inf_input", + inputs=[DECIMAL128_NEGATIVE_INFINITY, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject Decimal128 -Infinity input element", + ), + ZipTest( + id="decimal128_neg_zero_input", + inputs=[DECIMAL128_NEGATIVE_ZERO, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject Decimal128 -0 input element", + ), +] + +# --------------------------------------------------------------------------- +# Error: numeric boundary values as input element +# --------------------------------------------------------------------------- +BOUNDARY_ERROR_TESTS: list[ZipTest] = [ + ZipTest( + id="int32_max_input", + inputs=[INT32_MAX, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject INT32_MAX input element", + ), + ZipTest( + id="int32_min_input", + inputs=[INT32_MIN, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject INT32_MIN input element", + ), + ZipTest( + id="int64_max_input", + inputs=[INT64_MAX, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject INT64_MAX input element", + ), + ZipTest( + id="int64_min_input", + inputs=[INT64_MIN, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject INT64_MIN input element", + ), + ZipTest( + id="decimal128_max_input", + inputs=[DECIMAL128_MAX, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject DECIMAL128_MAX input element", + ), + ZipTest( + id="decimal128_min_input", + inputs=[DECIMAL128_MIN, [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject DECIMAL128_MIN input element", + ), +] + +# --------------------------------------------------------------------------- +# Error: string edge cases as input element +# --------------------------------------------------------------------------- +STRING_EDGE_ERROR_TESTS: list[ZipTest] = [ + ZipTest( + id="comma_separated_string_input", + inputs=["1, 2, 3", [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject comma-separated string", + ), + ZipTest( + id="json_like_string_input", + inputs=["[1, 2, 3]", [1]], + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Should reject JSON-like string", + ), +] + +# --------------------------------------------------------------------------- +# Error: invalid useLongestLength +# --------------------------------------------------------------------------- +USE_LONGEST_ERROR_TESTS: list[ZipTest] = [ + ZipTest( + id="use_longest_string", + inputs=[[1], [2]], + use_longest_length="true", + error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, + msg="Should reject string useLongestLength", + ), + ZipTest( + id="use_longest_int", + inputs=[[1], [2]], + use_longest_length=1, + error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, + msg="Should reject int useLongestLength", + ), + ZipTest( + id="use_longest_int_0", + inputs=[[1, 2]], + use_longest_length=0, + error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, + msg="Int 0 should error (not bool)", + ), + ZipTest( + id="use_longest_empty_string", + inputs=[[1, 2]], + use_longest_length="", + error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, + msg="Empty string should error (not bool)", + ), + ZipTest( + id="use_longest_array", + inputs=[[1, 2]], + use_longest_length=[], + error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, + msg="Empty array should error (not bool)", + ), + ZipTest( + id="use_longest_object", + inputs=[[1, 2]], + use_longest_length={"a": 1}, + error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, + msg="Object should error (not bool)", + ), + ZipTest( + id="use_longest_nan", + inputs=[[1, 2]], + use_longest_length=float("nan"), + error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, + msg="NaN should error (not bool)", + ), + ZipTest( + id="use_longest_infinity", + inputs=[[1, 2]], + use_longest_length=float("inf"), + error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, + msg="Infinity should error (not bool)", + ), +] + +# --------------------------------------------------------------------------- +# Error: invalid defaults +# --------------------------------------------------------------------------- +DEFAULTS_ERROR_TESTS: list[ZipTest] = [ + ZipTest( + id="defaults_without_use_longest", + inputs=[[1, 2], [3]], + defaults=[0, 0], + error_code=ZIP_DEFAULTS_WITHOUT_LONGEST_ERROR, + msg="Should reject defaults without useLongestLength", + ), + ZipTest( + id="defaults_without_longest_false", + inputs=[[1], [2]], + use_longest_length=False, + defaults=[0, 0], + error_code=ZIP_DEFAULTS_WITHOUT_LONGEST_ERROR, + msg="defaults with useLongestLength false should error", + ), + ZipTest( + id="defaults_length_mismatch", + inputs=[[1, 2], [3]], + use_longest_length=True, + defaults=[0], + error_code=ZIP_DEFAULTS_LENGTH_MISMATCH_ERROR, + msg="Should reject defaults with wrong length", + ), + ZipTest( + id="defaults_too_many", + inputs=[[1], [2]], + use_longest_length=True, + defaults=[0, 0, 0], + error_code=ZIP_DEFAULTS_LENGTH_MISMATCH_ERROR, + msg="Should reject defaults longer than inputs", + ), + ZipTest( + id="defaults_not_array", + inputs=[[1], [2]], + use_longest_length=True, + defaults="bad", + error_code=ZIP_DEFAULTS_NOT_ARRAY_ERROR, + msg="Should reject non-array defaults", + ), + ZipTest( + id="defaults_not_array_object", + inputs=[[1]], + use_longest_length=True, + defaults={"a": 1}, + error_code=ZIP_DEFAULTS_NOT_ARRAY_ERROR, + msg="defaults as object should error", + ), + ZipTest( + id="defaults_not_array_int", + inputs=[[1]], + use_longest_length=True, + defaults=1, + error_code=ZIP_DEFAULTS_NOT_ARRAY_ERROR, + msg="defaults as int should error", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_INPUT_ELEMENT_TESTS = ( + NOT_ARRAY_ELEMENT_TESTS + + SPECIAL_NUMERIC_ERROR_TESTS + + BOUNDARY_ERROR_TESTS + + STRING_EDGE_ERROR_TESTS + + USE_LONGEST_ERROR_TESTS + + DEFAULTS_ERROR_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_INPUT_ELEMENT_TESTS)) +def test_zip_not_array_insert(collection, test): + """Test $zip error with non-array input element from inserted documents.""" + expr = {"inputs": [f"$arr{i}" for i in range(len(test.inputs))]} + if test.use_longest_length is not None: + expr["useLongestLength"] = test.use_longest_length + if test.defaults is not None: + expr["defaults"] = test.defaults + doc = {f"arr{i}": arr for i, arr in enumerate(test.inputs)} + result = execute_expression_with_insert(collection, {"$zip": expr}, doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + + +TEST_SUBSET_FOR_LITERAL = [ + NOT_ARRAY_ELEMENT_TESTS[0], # string_input + NOT_ARRAY_ELEMENT_TESTS[-2], # non_array_second_position + SPECIAL_NUMERIC_ERROR_TESTS[0], # nan_input + BOUNDARY_ERROR_TESTS[0], # int32_max_input +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_zip_not_array_literal(collection, test): + """Test $zip error with non-array literal input element.""" + expr = {"inputs": [{"$literal": a} if isinstance(a, list) else a for a in test.inputs]} + if test.use_longest_length is not None: + expr["useLongestLength"] = test.use_longest_length + if test.defaults is not None: + expr["defaults"] = test.defaults + result = execute_expression(collection, {"$zip": expr}) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_expressions.py new file mode 100644 index 000000000..19134b5ba --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_expressions.py @@ -0,0 +1,250 @@ +""" +Expression and field path tests for $zip expression. + +Tests field path lookups, composite paths, system variables, +and null/missing propagation via expressions. +""" + +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 ( # noqa: E501 + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ZIP_REQUIRES_ARRAY_ELEMENT_ERROR +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Field path lookups +# --------------------------------------------------------------------------- +FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_field_path", + expression={"$zip": {"inputs": ["$a.b", "$a.c"]}}, + doc={"a": {"b": [1, 2], "c": [3, 4]}}, + expected=[[1, 3], [2, 4]], + msg="Should resolve nested field paths", + ), + ExpressionTestCase( + id="deeply_nested_field", + expression={"$zip": {"inputs": ["$a.b.c", "$a.b.d"]}}, + doc={"a": {"b": {"c": [10], "d": [20]}}}, + expected=[[10, 20]], + msg="Should resolve deeply nested field paths", + ), + ExpressionTestCase( + id="nonexistent_field_null", + expression={"$zip": {"inputs": ["$a.nonexistent", "$b"]}}, + doc={"a": {"missing": 1}, "b": [1]}, + expected=None, + msg="Non-existent field should propagate null", + ), + ExpressionTestCase( + id="same_field_twice", + expression={"$zip": {"inputs": ["$arr", "$arr"]}}, + doc={"arr": [1, 2, 3]}, + expected=[[1, 1], [2, 2], [3, 3]], + msg="Same field used twice should zip with itself", + ), +] + +# --------------------------------------------------------------------------- +# Composite array paths +# --------------------------------------------------------------------------- +COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="composite_array", + expression={"$zip": {"inputs": ["$x.y", "$x.z"]}}, + doc={"x": [{"y": 1, "z": 10}, {"y": 2, "z": 20}]}, + expected=[[1, 10], [2, 20]], + msg="Composite array path from array-of-objects", + ), + ExpressionTestCase( + id="array_index_path_resolves_empty", + expression={"$zip": {"inputs": ["$a.0", [1, 2]]}}, + doc={"a": [[1, 2], [3, 4]]}, + expected=[], + msg="Array index path resolves to [] (shortest)", + ), +] + +# --------------------------------------------------------------------------- +# $let and system variables +# --------------------------------------------------------------------------- +LET_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="let_variable", + expression={ + "$let": { + "vars": {"a": "$arr1", "b": "$arr2"}, + "in": {"$zip": {"inputs": ["$$a", "$$b"]}}, + } + }, + doc={"arr1": [1, 2], "arr2": [3, 4]}, + expected=[[1, 3], [2, 4]], + msg="Should work with $let variables", + ), + ExpressionTestCase( + id="root_variable", + expression={"$zip": {"inputs": ["$$ROOT.a", "$$ROOT.b"]}}, + doc={"_id": 1, "a": [1], "b": [2]}, + expected=[[1, 2]], + msg="Should work with $$ROOT", + ), + ExpressionTestCase( + id="current_variable", + expression={"$zip": {"inputs": ["$$CURRENT.a", "$$CURRENT.b"]}}, + doc={"_id": 2, "a": [1], "b": [2]}, + expected=[[1, 2]], + msg="$$CURRENT should be equivalent to field path", + ), +] + +# --------------------------------------------------------------------------- +# Null/missing via expression +# --------------------------------------------------------------------------- +NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="missing_field", + expression={"$zip": {"inputs": ["$nonexistent", [1]]}}, + doc={"other": 1}, + expected=None, + msg="Missing field should propagate null", + ), + ExpressionTestCase( + id="missing_input_type_is_null", + expression={"$type": {"$zip": {"inputs": ["$nonexistent", [1]]}}}, + doc={"x": 1}, + expected="null", + msg="Missing field should produce null type", + ), + ExpressionTestCase( + id="remove_variable", + expression={"$zip": {"inputs": ["$$REMOVE", [1]]}}, + doc={"x": 1}, + expected=None, + msg="$$REMOVE propagates null", + ), + ExpressionTestCase( + id="field_ref_non_array", + expression={"$zip": {"inputs": ["$a", [1]]}}, + doc={"a": 1}, + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="Field resolving to non-array should error", + ), + ExpressionTestCase( + id="missing_first_input", + expression={"$zip": {"inputs": ["$missing", [1, 2]]}}, + doc={"a": 1}, + expected=None, + msg="Missing first input returns null", + ), + ExpressionTestCase( + id="all_missing_fields", + expression={"$zip": {"inputs": ["$x", "$y"]}}, + doc={"_placeholder": 1}, + expected=None, + msg="All missing fields return null", + ), + ExpressionTestCase( + id="explicit_null_field", + expression={"$zip": {"inputs": ["$a", "$b"]}}, + doc={"a": None, "b": [1, 2]}, + expected=None, + msg="Explicit null field returns null", + ), + ExpressionTestCase( + id="null_with_longest_true", + expression={"$zip": {"inputs": ["$a", [1, 2]], "useLongestLength": True}}, + doc={"a": None}, + expected=None, + msg="Null input with longest true returns null", + ), + ExpressionTestCase( + id="null_with_defaults", + expression={ + "$zip": {"inputs": ["$a", [1, 2]], "useLongestLength": True, "defaults": [0, 0]} + }, + doc={"a": None}, + expected=None, + msg="Null input with defaults still returns null", + ), + ExpressionTestCase( + id="missing_field_as_element", + expression={"$zip": {"inputs": [["$not_exist", 2], [1, 2]]}}, + doc={"a": 1}, + expected=[[None, 1], [2, 2]], + msg="Missing field as element becomes null", + ), +] + +# --------------------------------------------------------------------------- +# Self-composition +# --------------------------------------------------------------------------- +SELF_COMPOSITION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_zip_full", + expression={ + "$zip": { + "inputs": [ + {"$zip": {"inputs": ["$a", "$b"]}}, + "$c", + ] + } + }, + doc={"a": [1, 2], "b": [3, 4], "c": ["x", "y"]}, + expected=[[[1, 3], "x"], [[2, 4], "y"]], + msg="Nested $zip as input works correctly", + ), +] + +# --------------------------------------------------------------------------- +# Field path as element in input array +# --------------------------------------------------------------------------- +FIELD_PATH_ELEMENT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="field_path_as_element", + expression={"$zip": {"inputs": [["$a", "$b"], [1, 2]]}}, + doc={"a": 10, "b": 20}, + expected=[[10, 1], [20, 2]], + msg="Field paths as elements resolve correctly", + ), + ExpressionTestCase( + id="nested_field_path_as_element", + expression={"$zip": {"inputs": [["$foo.bar", "$b"], [1, 2]]}}, + doc={"foo": {"bar": 10}, "b": 20}, + expected=[[10, 1], [20, 2]], + msg="Nested field path as element resolves correctly", + ), + ExpressionTestCase( + id="field_path_in_defaults", + expression={ + "$zip": {"inputs": ["$a", "$b"], "useLongestLength": True, "defaults": ["$c", "$d"]} + }, + doc={"a": [1, 2, 3], "b": ["x"], "c": "default_a", "d": "default_b"}, + expected=[[1, "x"], [2, "default_b"], [3, "default_b"]], + msg="Field paths in defaults resolve from document", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_EXPR_TESTS = ( + FIELD_LOOKUP_TESTS + + COMPOSITE_PATH_TESTS + + LET_TESTS + + NULL_MISSING_EXPR_TESTS + + SELF_COMPOSITION_TESTS + + FIELD_PATH_ELEMENT_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_EXPR_TESTS)) +def test_zip_expression(collection, test): + """Test $zip with field paths and expressions.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_expression_zip.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_expression_zip.py index 50f19ac0b..8cd4d56da 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_expression_zip.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_expression_zip.py @@ -28,4 +28,4 @@ def test_smoke_expression_zip(collection): ) expected = [{"_id": 1, "zipped": [[1, 10], [2, 20]]}, {"_id": 2, "zipped": [[3, 30], [4, 40]]}] - assertSuccess(result, expected, msg="Should support $zip expression") + assertSuccess(result, expected, "Should support $zip expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/zip_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/zip_common.py new file mode 100644 index 000000000..40c5f6bf7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/zip_common.py @@ -0,0 +1,17 @@ +""" +Shared test infrastructure for $zip expression tests. +""" + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class ZipTest(BaseTestCase): + """Test case for $zip operator.""" + + inputs: Any = None + use_longest_length: Any = None + defaults: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_map.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_map.py new file mode 100644 index 000000000..131fba8dd --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_map.py @@ -0,0 +1,94 @@ +""" +Combination tests for $map 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 ( + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.parametrize import pytest_params + +MAP_COMBINATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="map_on_range", + expression={ + "$map": {"input": {"$range": [0, 5]}, "in": {"$multiply": ["$$this", "$$this"]}} + }, + doc={"_placeholder": 1}, + expected=[0, 1, 4, 9, 16], + msg="Should map squares over $range result", + ), + ExpressionTestCase( + id="map_with_concatArrays", + expression={ + "$concatArrays": [ + {"$map": {"input": "$a", "in": {"$multiply": ["$$this", 2]}}}, + {"$map": {"input": "$b", "in": {"$multiply": ["$$this", 3]}}}, + ] + }, + doc={"a": [1, 2], "b": [3, 4]}, + expected=[2, 4, 9, 12], + msg="Should concatenate two mapped arrays", + ), + ExpressionTestCase( + id="map_with_filter", + expression={ + "$map": { + "input": {"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 2]}}}, + "in": {"$multiply": ["$$this", 10]}, + } + }, + doc={"arr": [1, 2, 3, 4, 5]}, + expected=[30, 40, 50], + msg="Should map over filtered array", + ), + ExpressionTestCase( + id="map_result_into_reduce", + expression={ + "$reduce": { + "input": {"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, + "initialValue": 0, + "in": {"$add": ["$$value", "$$this"]}, + } + }, + doc={"arr": [1, 2, 3]}, + expected=12, + msg="$reduce on $map result should sum doubled values", + ), + ExpressionTestCase( + id="map_3_level_nested", + expression={ + "$map": { + "input": [[[1]]], + "as": "a", + "in": { + "$map": { + "input": "$$a", + "as": "b", + "in": { + "$map": { + "input": "$$b", + "in": {"$add": ["$$this", 100]}, + } + }, + } + }, + } + }, + doc={"_placeholder": 1}, + expected=[[[101]]], + msg="3-level nested $map should work", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(MAP_COMBINATION_TESTS)) +def test_map_combination(collection, test): + """Test $map composed with other operators.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assertResult(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_range.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_range.py new file mode 100644 index 000000000..8ea6bc843 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_range.py @@ -0,0 +1,87 @@ +""" +Combination tests for $range 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 ( + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.parametrize import pytest_params + +RANGE_COMBINATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="reverseArray_on_range", + expression={"$reverseArray": {"$range": ["$start", "$end"]}}, + doc={"start": 0, "end": 5}, + expected=[4, 3, 2, 1, 0], + msg="$reverseArray on $range result", + ), + ExpressionTestCase( + id="concatArrays_two_ranges", + expression={"$concatArrays": [{"$range": [0, 3]}, {"$range": [3, 6]}]}, + doc={"x": 1}, + expected=[0, 1, 2, 3, 4, 5], + msg="$concatArrays on two $range results", + ), + ExpressionTestCase( + id="in_on_range", + expression={"$in": [5, {"$range": ["$start", "$end"]}]}, + doc={"start": 0, "end": 10}, + expected=True, + msg="$in on $range result", + ), + ExpressionTestCase( + id="isArray_on_range", + expression={"$isArray": {"$range": [0, 3]}}, + doc={"x": 1}, + expected=True, + msg="$isArray on $range result should return true", + ), + ExpressionTestCase( + id="in_miss_on_range", + expression={"$in": [5, {"$range": [0, 5]}]}, + doc={"x": 1}, + expected=False, + msg="5 should not be in [0..4] (exclusive end)", + ), + ExpressionTestCase( + id="self_nesting_start", + expression={"$range": [{"$arrayElemAt": [{"$range": [2, 5]}, 0]}, 10]}, + doc={"x": 1}, + expected=[2, 3, 4, 5, 6, 7, 8, 9], + msg="Start from inner range", + ), + ExpressionTestCase( + id="self_nesting_end", + expression={"$range": [0, {"$size": {"$range": [0, 5]}}]}, + doc={"x": 1}, + expected=[0, 1, 2, 3, 4], + msg="End from size of inner range", + ), + ExpressionTestCase( + id="indexOfArray_on_range", + expression={"$indexOfArray": [{"$range": [0, 10]}, 7]}, + doc={"x": 1}, + expected=7, + msg="Index of 7 in range 0..9 should be 7", + ), + ExpressionTestCase( + id="output_type_is_int", + expression={"$type": {"$arrayElemAt": [{"$range": [0, 1]}, 0]}}, + doc={"x": 1}, + expected="int", + msg="Output element should be int type", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(RANGE_COMBINATION_TESTS)) +def test_range_combination(collection, test): + """Test $range composed with other operators.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assertResult(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_zip.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_zip.py new file mode 100644 index 000000000..2dd359748 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_zip.py @@ -0,0 +1,124 @@ +""" +Combination tests for $zip 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 ( + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import FLOAT_NAN + +ZIP_COMBINATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="zip_on_sortArray", + expression={"$zip": {"inputs": [{"$sortArray": {"input": "$a", "sortBy": 1}}, "$b"]}}, + doc={"a": [3, 1, 2], "b": [10, 20, 30]}, + expected=[[1, 10], [2, 20], [3, 30]], + msg="Should zip $sortArray result with array", + ), + ExpressionTestCase( + id="zip_on_concatArrays", + expression={"$zip": {"inputs": [{"$concatArrays": ["$a", "$b"]}, "$c"]}}, + doc={"a": [1], "b": [2], "c": [10, 20]}, + expected=[[1, 10], [2, 20]], + msg="Should zip $concatArrays result with array", + ), + ExpressionTestCase( + id="zip_on_range", + expression={"$zip": {"inputs": [{"$range": [0, 3]}, {"$range": [10, 13]}]}}, + doc={"x": 1}, + expected=[[0, 10], [1, 11], [2, 12]], + msg="Should zip two $range results", + ), + ExpressionTestCase( + id="arrayElemAt_on_zip", + expression={"$arrayElemAt": [{"$zip": {"inputs": ["$a", "$b"]}}, 1]}, + doc={"a": [1, 2, 3], "b": [10, 20, 30]}, + expected=[2, 20], + msg="$arrayElemAt on $zip result", + ), + ExpressionTestCase( + id="zip_matrix_transpose_2x3", + expression={ + "$zip": { + "inputs": [ + {"$arrayElemAt": ["$matrix", 0]}, + {"$arrayElemAt": ["$matrix", 1]}, + ] + } + }, + doc={"matrix": [[0, 1, 2], [3, 4, 5]]}, + expected=[[0, 3], [1, 4], [2, 5]], + msg="2x3 matrix transposed to 3x2", + ), + ExpressionTestCase( + id="zip_index_preservation", + expression={"$zip": {"inputs": ["$arr", {"$range": [0, {"$size": "$arr"}]}]}}, + doc={"arr": ["a", "b", "c"]}, + expected=[["a", 0], ["b", 1], ["c", 2]], + msg="Elements paired with indices", + ), + ExpressionTestCase( + id="zip_reduce_on_output", + expression={ + "$reduce": { + "input": {"$zip": {"inputs": ["$a", "$b"]}}, + "initialValue": 0, + "in": {"$add": ["$$value", {"$arrayElemAt": ["$$this", 1]}]}, + } + }, + doc={"a": [1, 2, 3], "b": [10, 20, 30]}, + expected=60, + msg="$reduce sums second elements of zipped pairs", + ), + ExpressionTestCase( + id="zip_output_is_array", + expression={"$isArray": {"$zip": {"inputs": ["$a", "$b"]}}}, + doc={"a": [1, 2], "b": ["a", "b"]}, + expected=True, + msg="Output is an array", + ), + ExpressionTestCase( + id="float_nan_preserved", + expression={"$arrayElemAt": [{"$arrayElemAt": [{"$zip": {"inputs": ["$a", "$b"]}}, 0]}, 0]}, + doc={"a": [FLOAT_NAN], "b": [1]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="Float NaN element preserved after zipping", + ), + ExpressionTestCase( + id="default_float_nan", + expression={ + "$arrayElemAt": [ + { + "$arrayElemAt": [ + { + "$zip": { + "inputs": ["$a", "$b"], + "useLongestLength": True, + "defaults": [0, FLOAT_NAN], + } + }, + 1, + ] + }, + 1, + ] + }, + doc={"a": [1, 2], "b": [10]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="Should use float NaN as default value", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ZIP_COMBINATION_TESTS)) +def test_zip_combination(collection, test): + """Test $zip composed with other operators.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) From 34d75dba3335d857c05308f3014a17a434f20309 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 7 Jul 2026 16:04:09 -0700 Subject: [PATCH 02/17] fix tests to run against mongodb 8.2 Signed-off-by: Alina (Xi) Li --- .../map/test_expression_map_as_errors.py | 4 +- .../map/test_expression_map_bson_types.py | 51 +++++++++++++++---- .../map/test_expression_map_core_behavior.py | 6 ++- .../array/map/test_expression_map_errors.py | 10 ++-- .../map/test_expression_map_expressions.py | 6 ++- .../test_expression_map_structure_errors.py | 4 +- .../range/test_expression_range_boundary.py | 10 ++-- .../test_expression_range_core_behavior.py | 10 ++-- .../range/test_expression_range_errors.py | 20 +++++--- .../test_expression_range_expressions.py | 6 ++- ...xpression_zip_argument_structure_errors.py | 6 ++- .../zip/test_expression_zip_bson_types.py | 30 +++++++---- .../zip/test_expression_zip_core_behavior.py | 10 ++-- .../array/zip/test_expression_zip_errors.py | 14 +++-- .../zip/test_expression_zip_expressions.py | 6 ++- .../test_expressions_combination_map.py | 8 +-- .../test_expressions_combination_range.py | 8 +-- .../test_expressions_combination_zip.py | 8 +-- 18 files changed, 149 insertions(+), 68 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_as_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_as_errors.py index 9e3f9d638..94fcacb8c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_as_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_as_errors.py @@ -13,9 +13,9 @@ ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import FAILED_TO_PARSE_ERROR from documentdb_tests.framework.parametrize import pytest_params @@ -125,4 +125,4 @@ def test_map_invalid_as(collection, test): """Test $map with invalid 'as' parameter values.""" result = execute_expression(collection, test.expression) - assertResult(result, error_code=test.error_code, msg=test.msg) + assert_expression_result(result, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_bson_types.py index c6faa908e..3e01252b0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_bson_types.py @@ -5,7 +5,7 @@ including special numeric values and boundary values. """ -from datetime import datetime +from datetime import datetime, timezone from uuid import UUID import pytest @@ -15,9 +15,9 @@ ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( DECIMAL128_INFINITY, @@ -54,8 +54,16 @@ ExpressionTestCase( id="datetime_values", expression={"$map": {"input": "$arr", "in": "$$this"}}, - doc={"arr": [datetime(2024, 1, 1), datetime(2024, 6, 1)]}, - expected=[datetime(2024, 1, 1), datetime(2024, 6, 1)], + 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( @@ -131,8 +139,18 @@ ExpressionTestCase( id="mixed_dates_and_ids", expression={"$map": {"input": "$arr", "in": "$$this"}}, - doc={"arr": [datetime(2024, 1, 1), ObjectId("000000000000000000000001"), Timestamp(1, 0)]}, - expected=[datetime(2024, 1, 1), ObjectId("000000000000000000000001"), Timestamp(1, 0)], + 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", ), ] @@ -224,7 +242,12 @@ "in": {"$dateToString": {"format": "%Y-%m-%d", "date": "$$this"}}, } }, - doc={"arr": [datetime(2024, 1, 1), datetime(2024, 6, 15)]}, + doc={ + "arr": [ + datetime(2024, 1, 1, tzinfo=timezone.utc), + datetime(2024, 6, 15, tzinfo=timezone.utc), + ] + }, expected=["2024-01-01", "2024-06-15"], msg="$dateToString on datetime array", ), @@ -259,8 +282,16 @@ ExpressionTestCase( id="add_millis_to_datetime", expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 86400000]}}}, - doc={"arr": [datetime(2024, 1, 1), datetime(2024, 6, 1)]}, - expected=[datetime(2024, 1, 2), datetime(2024, 6, 2)], + doc={ + "arr": [ + datetime(2024, 1, 1, tzinfo=timezone.utc), + datetime(2024, 6, 1, tzinfo=timezone.utc), + ] + }, + expected=[ + datetime(2024, 1, 2, tzinfo=timezone.utc), + datetime(2024, 6, 2, tzinfo=timezone.utc), + ], msg="Add one day in millis to datetime array", ), ExpressionTestCase( @@ -288,4 +319,4 @@ def test_map_bson_insert(collection, test): """Test $map BSON types with values from inserted documents.""" result = execute_expression_with_insert(collection, test.expression, test.doc) - assertResult(result, expected=test.expected, msg=test.msg) + assert_expression_result(result, expected=test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_core_behavior.py index 6fa5369d4..403c66182 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_core_behavior.py @@ -12,9 +12,9 @@ ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.parametrize import pytest_params # --------------------------------------------------------------------------- @@ -330,4 +330,6 @@ def test_map_insert(collection, test): """Test $map with values from inserted documents.""" result = execute_expression_with_insert(collection, test.expression, test.doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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/map/test_expression_map_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_errors.py index 8ac8afb65..8ed3cb35c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_errors.py @@ -6,7 +6,7 @@ Note: $map 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 @@ -15,9 +15,9 @@ ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( MAP_INPUT_NOT_ARRAY_ERROR, ) @@ -116,7 +116,7 @@ ExpressionTestCase( id="datetime_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, - doc={"arr": datetime(2024, 1, 1)}, + doc={"arr": datetime(2024, 1, 1, tzinfo=timezone.utc)}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, msg="Should reject datetime input", ), @@ -284,4 +284,6 @@ def test_map_not_array_insert(collection, test): """Test $map error with non-array input from inserted documents.""" result = execute_expression_with_insert(collection, test.expression, test.doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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/map/test_expression_map_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_expressions.py index fc02d1be8..f1b896741 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_expressions.py @@ -11,9 +11,9 @@ ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.parametrize import pytest_params # --------------------------------------------------------------------------- @@ -252,4 +252,6 @@ def test_map_expression(collection, test): """Test $map with field paths and expressions.""" result = execute_expression_with_insert(collection, test.expression, test.doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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/map/test_expression_map_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_structure_errors.py index 8f854a1d7..5c716fe88 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_structure_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_structure_errors.py @@ -11,9 +11,9 @@ ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( EXPRESSION_NON_OBJECT_ARG_ERROR, MAP_MISSING_IN_ERROR, @@ -116,4 +116,4 @@ def test_map_structure_error(collection, test): """Test $map argument structure validation.""" result = execute_expression(collection, test.expression) - assertResult(result, error_code=test.error_code, msg=test.msg) + assert_expression_result(result, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_boundary.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_boundary.py index 4e009cd4a..6d9bd538b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_boundary.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_boundary.py @@ -10,10 +10,10 @@ RangeTest, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import INT32_MAX, INT32_MAX_MINUS_1, INT32_MIN @@ -97,7 +97,9 @@ def test_range_boundary_insert(collection, test): args.append("$step") doc["step"] = test.step result = execute_expression_with_insert(collection, {"$range": args}, doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) @pytest.mark.parametrize("test", pytest_params(ALL_BOUNDARY_TESTS)) @@ -107,4 +109,6 @@ def test_range_boundary_literal(collection, test): if test.step is not None: args.append(test.step) result = execute_expression(collection, {"$range": args}) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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/range/test_expression_range_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_core_behavior.py index 049571761..6be425647 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_core_behavior.py @@ -12,10 +12,10 @@ RangeTest, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.parametrize import pytest_params # --------------------------------------------------------------------------- @@ -475,7 +475,9 @@ def test_range_insert(collection, test): args.append("$step") doc["step"] = test.step result = execute_expression_with_insert(collection, {"$range": args}, doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) TEST_SUBSET_FOR_LITERAL = [ @@ -493,4 +495,6 @@ def test_range_literal(collection, test): if test.step is not None: args.append(test.step) result = execute_expression(collection, {"$range": args}) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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/range/test_expression_range_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_errors.py index e75bfab98..8fa69e9c1 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_errors.py @@ -6,7 +6,7 @@ """ -from datetime import datetime +from datetime import datetime, timezone import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp @@ -15,10 +15,10 @@ RangeTest, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( EXPRESSION_ARITY_ERROR, RANGE_END_NOT_INT32_ERROR, @@ -91,7 +91,7 @@ ), RangeTest( id="datetime_start", - start=datetime(2024, 1, 1), + start=datetime(2024, 1, 1, tzinfo=timezone.utc), end=5, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject datetime start", @@ -182,7 +182,7 @@ RangeTest( id="datetime_end", start=0, - end=datetime(2024, 1, 1), + end=datetime(2024, 1, 1, tzinfo=timezone.utc), error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject datetime end", ), @@ -271,7 +271,7 @@ id="datetime_step", start=0, end=5, - step=datetime(2024, 1, 1), + step=datetime(2024, 1, 1, tzinfo=timezone.utc), error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject datetime step", ), @@ -663,7 +663,9 @@ def test_range_error_literal(collection, test): if test.step is not None: args.append(test.step) result = execute_expression(collection, {"$range": args}) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) @@ -675,7 +677,9 @@ def test_range_error_insert(collection, test): args.append("$step") doc["step"] = test.step result = execute_expression_with_insert(collection, {"$range": args}, doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) # --------------------------------------------------------------------------- @@ -692,4 +696,4 @@ def test_range_error_insert(collection, test): def test_range_arity_error(collection, expr): """Test $range errors with wrong number of arguments.""" result = execute_expression(collection, expr) - assertResult(result, error_code=EXPRESSION_ARITY_ERROR) + assert_expression_result(result, error_code=EXPRESSION_ARITY_ERROR) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_expressions.py index de4e42042..38ab8363d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_expressions.py @@ -11,9 +11,9 @@ ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( RANGE_END_NOT_NUMERIC_ERROR, RANGE_START_NOT_INT32_ERROR, @@ -166,4 +166,6 @@ def test_range_expression(collection, test): """Test $range with field paths and expressions.""" result = execute_expression_with_insert(collection, test.expression, test.doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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/zip/test_expression_zip_argument_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_argument_structure_errors.py index 78967ade8..f668333f7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_argument_structure_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_argument_structure_errors.py @@ -13,9 +13,9 @@ ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( ZIP_INPUT_ELEMENT_NOT_ARRAY_ERROR, ZIP_INPUTS_NOT_ARRAY_ERROR, @@ -130,4 +130,6 @@ def test_zip_argument_handling(collection, test): """Test $zip argument structure validation.""" result = execute_expression(collection, test.expression) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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/zip/test_expression_zip_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_bson_types.py index 280f94f81..480a5ac87 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_bson_types.py @@ -4,7 +4,7 @@ Tests that various BSON types are preserved when zipping arrays. """ -from datetime import datetime +from datetime import datetime, timezone from uuid import UUID import pytest @@ -14,10 +14,10 @@ ZipTest, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( DECIMAL128_INFINITY, @@ -50,8 +50,13 @@ ), ZipTest( id="datetime_values", - inputs=[[datetime(2024, 1, 1)], [datetime(2024, 6, 1)]], - expected=[[datetime(2024, 1, 1), datetime(2024, 6, 1)]], + inputs=[ + [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", ), ZipTest( @@ -173,10 +178,13 @@ ), ZipTest( id="default_datetime", - inputs=[[1, 2], [datetime(2024, 1, 1)]], + inputs=[[1, 2], [datetime(2024, 1, 1, tzinfo=timezone.utc)]], use_longest_length=True, - defaults=[0, datetime(1970, 1, 1)], - expected=[[1, datetime(2024, 1, 1)], [2, datetime(1970, 1, 1)]], + defaults=[0, datetime(1970, 1, 1, tzinfo=timezone.utc)], + expected=[ + [1, datetime(2024, 1, 1, tzinfo=timezone.utc)], + [2, datetime(1970, 1, 1, tzinfo=timezone.utc)], + ], msg="Should use datetime default value", ), ZipTest( @@ -308,7 +316,9 @@ def test_zip_bson_insert(collection, test): expr["defaults"] = test.defaults doc = {f"arr{i}": arr for i, arr in enumerate(test.inputs)} result = execute_expression_with_insert(collection, {"$zip": expr}, doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) TEST_SUBSET_FOR_LITERAL = [ @@ -328,4 +338,6 @@ def test_zip_bson_literal(collection, test): if test.defaults is not None: expr["defaults"] = test.defaults result = execute_expression(collection, {"$zip": expr}) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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/zip/test_expression_zip_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_core_behavior.py index f3297bf2c..a2dd61547 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_core_behavior.py @@ -13,10 +13,10 @@ ZipTest, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.parametrize import pytest_params # --------------------------------------------------------------------------- @@ -418,7 +418,9 @@ def test_zip_insert(collection, test): expr["defaults"] = test.defaults doc = {f"arr{i}": arr for i, arr in enumerate(test.inputs)} result = execute_expression_with_insert(collection, {"$zip": expr}, doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) TEST_SUBSET_FOR_LITERAL = [ @@ -439,4 +441,6 @@ def test_zip_literal(collection, test): if test.defaults is not None: expr["defaults"] = test.defaults result = execute_expression(collection, {"$zip": expr}) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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/zip/test_expression_zip_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_errors.py index b745f5d7a..73d699e8c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_errors.py @@ -5,7 +5,7 @@ defaults without useLongestLength, and defaults length mismatch. """ -from datetime import datetime +from datetime import datetime, timezone import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp @@ -14,10 +14,10 @@ ZipTest, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( ZIP_DEFAULTS_LENGTH_MISMATCH_ERROR, ZIP_DEFAULTS_NOT_ARRAY_ERROR, @@ -109,7 +109,7 @@ ), ZipTest( id="datetime_input", - inputs=[datetime(2024, 1, 1), [1]], + inputs=[datetime(2024, 1, 1, tzinfo=timezone.utc), [1]], error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject datetime input element", ), @@ -417,7 +417,9 @@ def test_zip_not_array_insert(collection, test): expr["defaults"] = test.defaults doc = {f"arr{i}": arr for i, arr in enumerate(test.inputs)} result = execute_expression_with_insert(collection, {"$zip": expr}, doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) TEST_SUBSET_FOR_LITERAL = [ @@ -437,4 +439,6 @@ def test_zip_not_array_literal(collection, test): if test.defaults is not None: expr["defaults"] = test.defaults result = execute_expression(collection, {"$zip": expr}) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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/zip/test_expression_zip_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_expressions.py index 19134b5ba..a9a0889ff 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_expressions.py @@ -11,9 +11,9 @@ ExpressionTestCase, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ZIP_REQUIRES_ARRAY_ELEMENT_ERROR from documentdb_tests.framework.parametrize import pytest_params @@ -247,4 +247,6 @@ def test_zip_expression(collection, test): """Test $zip with field paths and expressions.""" result = execute_expression_with_insert(collection, test.expression, test.doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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_map.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_map.py index 131fba8dd..a59530606 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_map.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_map.py @@ -7,10 +7,10 @@ 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 ( +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.parametrize import pytest_params MAP_COMBINATION_TESTS: list[ExpressionTestCase] = [ @@ -91,4 +91,6 @@ def test_map_combination(collection, test): """Test $map composed with other operators.""" result = execute_expression_with_insert(collection, test.expression, test.doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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_range.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_range.py index 8ea6bc843..92b911224 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_range.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_range.py @@ -7,10 +7,10 @@ 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 ( +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.parametrize import pytest_params RANGE_COMBINATION_TESTS: list[ExpressionTestCase] = [ @@ -84,4 +84,6 @@ def test_range_combination(collection, test): """Test $range composed with other operators.""" result = execute_expression_with_insert(collection, test.expression, test.doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + 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_zip.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_zip.py index 2dd359748..00058490e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_zip.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_zip.py @@ -7,10 +7,10 @@ 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 ( +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 + assert_expression_result, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import FLOAT_NAN @@ -121,4 +121,6 @@ def test_zip_combination(collection, test): """Test $zip composed with other operators.""" result = execute_expression_with_insert(collection, test.expression, test.doc) - assertResult(result, expected=test.expected, error_code=test.error_code, msg=test.msg) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) From 5bbdc78d51aa6681937177d314cf4977d82ae896 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 7 Jul 2026 16:05:36 -0700 Subject: [PATCH 03/17] rename prefix file name Signed-off-by: Alina (Xi) Li --- .../{test_expression_map_as_errors.py => test_map_as_errors.py} | 0 .../{test_expression_map_bson_types.py => test_map_bson_types.py} | 0 ..._expression_map_core_behavior.py => test_map_core_behavior.py} | 0 .../map/{test_expression_map_errors.py => test_map_errors.py} | 0 ...test_expression_map_expressions.py => test_map_expressions.py} | 0 ...ssion_map_structure_errors.py => test_map_structure_errors.py} | 0 .../array/map/{test_smoke_expression_map.py => test_smoke_map.py} | 0 .../{test_expression_range_boundary.py => test_range_boundary.py} | 0 ...ression_range_core_behavior.py => test_range_core_behavior.py} | 0 .../{test_expression_range_errors.py => test_range_errors.py} | 0 ..._expression_range_expressions.py => test_range_expressions.py} | 0 .../range/{test_smoke_expression_range.py => test_smoke_range.py} | 0 .../array/zip/{test_smoke_expression_zip.py => test_smoke_zip.py} | 0 ..._structure_errors.py => test_zip_argument_structure_errors.py} | 0 .../{test_expression_zip_bson_types.py => test_zip_bson_types.py} | 0 ..._expression_zip_core_behavior.py => test_zip_core_behavior.py} | 0 .../zip/{test_expression_zip_errors.py => test_zip_errors.py} | 0 ...test_expression_zip_expressions.py => test_zip_expressions.py} | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/map/{test_expression_map_as_errors.py => test_map_as_errors.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/map/{test_expression_map_bson_types.py => test_map_bson_types.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/map/{test_expression_map_core_behavior.py => test_map_core_behavior.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/map/{test_expression_map_errors.py => test_map_errors.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/map/{test_expression_map_expressions.py => test_map_expressions.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/map/{test_expression_map_structure_errors.py => test_map_structure_errors.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/map/{test_smoke_expression_map.py => test_smoke_map.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/range/{test_expression_range_boundary.py => test_range_boundary.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/range/{test_expression_range_core_behavior.py => test_range_core_behavior.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/range/{test_expression_range_errors.py => test_range_errors.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/range/{test_expression_range_expressions.py => test_range_expressions.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/range/{test_smoke_expression_range.py => test_smoke_range.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/{test_smoke_expression_zip.py => test_smoke_zip.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/{test_expression_zip_argument_structure_errors.py => test_zip_argument_structure_errors.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/{test_expression_zip_bson_types.py => test_zip_bson_types.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/{test_expression_zip_core_behavior.py => test_zip_core_behavior.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/{test_expression_zip_errors.py => test_zip_errors.py} (100%) rename documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/{test_expression_zip_expressions.py => test_zip_expressions.py} (100%) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_as_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_as_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_bson_types.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_core_behavior.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_expressions.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_expression_map_structure_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_expression_map.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_map.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_expression_map.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_map.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_boundary.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_boundary.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_core_behavior.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_expression_range_expressions.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_expression_range.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_range.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_expression_range.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_range.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_expression_zip.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_zip.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_expression_zip.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_zip.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_argument_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_argument_structure_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_bson_types.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_core_behavior.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_expression_zip_expressions.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py From c9f2723b0738f55d8f867a54a42bbcdd0e990c0b Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 7 Jul 2026 16:45:08 -0700 Subject: [PATCH 04/17] update according to review guide Signed-off-by: Alina (Xi) Li --- .../array/map/test_map_bson_types.py | 16 +++------- .../array/map/test_map_core_behavior.py | 26 +++-------------- .../expressions/array/map/test_map_errors.py | 8 ----- .../array/map/test_map_expressions.py | 20 ++++--------- .../array/map/test_map_structure_errors.py | 11 ++----- .../array/range/test_range_boundary.py | 4 --- .../array/range/test_range_core_behavior.py | 22 ++------------ .../array/range/test_range_errors.py | 24 ++------------- .../array/range/test_range_expressions.py | 10 ++----- .../zip/test_zip_argument_structure_errors.py | 4 +-- .../array/zip/test_zip_bson_types.py | 15 ++-------- .../array/zip/test_zip_core_behavior.py | 29 ++++--------------- .../expressions/array/zip/test_zip_errors.py | 14 --------- .../array/zip/test_zip_expressions.py | 16 ++-------- 14 files changed, 35 insertions(+), 184 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py index 3e01252b0..ab9af5152 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py @@ -33,9 +33,8 @@ INT64_MIN, ) -# --------------------------------------------------------------------------- # BSON types preserved via identity map -# --------------------------------------------------------------------------- +# Property [Type Preservation]: $map preserves each element's BSON type. BSON_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="int64_values", @@ -125,9 +124,8 @@ ), ] -# --------------------------------------------------------------------------- # Mixed BSON types -# --------------------------------------------------------------------------- +# Property [Mixed Types]: $map processes arrays with mixed BSON types. MIXED_BSON_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="mixed_bson_types", @@ -155,9 +153,8 @@ ), ] -# --------------------------------------------------------------------------- # Special numeric values as elements -# --------------------------------------------------------------------------- +# Property [Special Numerics]: $map preserves NaN, Infinity, and boundary values. SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="infinity_values", @@ -189,9 +186,7 @@ ), ] -# --------------------------------------------------------------------------- # Decimal128 precision preservation -# --------------------------------------------------------------------------- DECIMAL128_PRECISION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="decimal128_trailing_zeros", @@ -209,9 +204,8 @@ ), ] -# --------------------------------------------------------------------------- # BSON type transformations -# --------------------------------------------------------------------------- +# Property [Type Transform]: $map transforms elements using type-specific operations. BSON_TRANSFORM_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="multiply_int64", @@ -303,9 +297,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_BSON_TESTS = ( BSON_TYPE_TESTS + MIXED_BSON_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py index 403c66182..9c079edb7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py @@ -17,9 +17,8 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Success: basic mapping -# --------------------------------------------------------------------------- +# Property [Basic Transform]: $map applies an expression to each element. BASIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="multiply_each", @@ -93,9 +92,8 @@ ), ] -# --------------------------------------------------------------------------- # Success: nested arrays (map does not flatten) -# --------------------------------------------------------------------------- +# Property [Nested Arrays]: $map operates on nested array structures. NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_arrays_identity", @@ -120,9 +118,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: elements with null -# --------------------------------------------------------------------------- NULL_ELEMENT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="null_elements_identity", @@ -140,9 +136,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: wrap each element -# --------------------------------------------------------------------------- WRAP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="wrap_in_array", @@ -160,9 +154,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: type conversion in expression -# --------------------------------------------------------------------------- TYPE_CONVERSION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="to_string", @@ -180,9 +172,8 @@ ), ] -# --------------------------------------------------------------------------- # Success: large array -# --------------------------------------------------------------------------- +# Property [Large Arrays]: $map handles large arrays. LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="large_array_1000", @@ -193,9 +184,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: order preservation and duplicates -# --------------------------------------------------------------------------- ORDER_DUPLICATE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="preserves_order", @@ -221,9 +210,8 @@ ] -# --------------------------------------------------------------------------- # Success: null element propagation -# --------------------------------------------------------------------------- +# Property [Null Propagation]: $zip returns null when inputs is null. NULL_PROPAGATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="null_add_propagation", @@ -255,9 +243,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: conditional in expressions -# --------------------------------------------------------------------------- CONDITIONAL_IN_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="cond_classify", @@ -282,9 +268,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: complex nested in expressions -# --------------------------------------------------------------------------- NESTED_IN_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_multiply_add", @@ -309,9 +293,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( BASIC_TESTS + NESTED_ARRAY_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py index 8ed3cb35c..b6e0b3632 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py @@ -39,9 +39,7 @@ INT64_MIN, ) -# --------------------------------------------------------------------------- # Error: non-array input — standard BSON types -# --------------------------------------------------------------------------- NOT_ARRAY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="string_input", @@ -157,9 +155,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: special float/Decimal128 values -# --------------------------------------------------------------------------- SPECIAL_NUMERIC_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nan_input", @@ -226,9 +222,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: numeric boundary values -# --------------------------------------------------------------------------- BOUNDARY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="int32_max_input", @@ -274,9 +268,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = NOT_ARRAY_ERROR_TESTS + SPECIAL_NUMERIC_ERROR_TESTS + BOUNDARY_ERROR_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py index f1b896741..0ebe3fb09 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py @@ -16,9 +16,8 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Field path lookups -# --------------------------------------------------------------------------- +# Property [Field Lookup]: $map resolves field paths in expressions. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_field_path", @@ -71,9 +70,8 @@ ), ] -# --------------------------------------------------------------------------- # $let and system variables -# --------------------------------------------------------------------------- +# Property [Variables]: $map works with $let and system variables. LET_AND_VARIABLE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="let_variable", @@ -103,9 +101,8 @@ ), ] -# --------------------------------------------------------------------------- # Null/missing via expression -# --------------------------------------------------------------------------- +# Property [Null/Missing Fields]: $map handles null and missing field paths. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="missing_field", @@ -137,9 +134,8 @@ ), ] -# --------------------------------------------------------------------------- # Nested $map -# --------------------------------------------------------------------------- +# Property [Nested Map]: $map can be nested inside another $map. NESTED_MAP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_map", @@ -163,9 +159,8 @@ ] -# --------------------------------------------------------------------------- # $map within $reduce and vice versa -# --------------------------------------------------------------------------- +# Property [Reduce Interaction]: $map output works with $reduce. REDUCE_INTERACTION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="map_within_reduce", @@ -202,9 +197,8 @@ ), ] -# --------------------------------------------------------------------------- # $$ROOT returning full document, $$REMOVE behavior -# --------------------------------------------------------------------------- +# Property [System Variables]: $map works with $$ROOT and $$CURRENT. SYSTEM_VAR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="root_returns_full_doc", @@ -235,9 +229,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/map/test_map_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py index 5c716fe88..e4b7f1aa1 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py @@ -22,9 +22,8 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Error: non-object argument -# --------------------------------------------------------------------------- +# Property [Object Argument]: $map rejects non-object arguments. NON_OBJECT_ARG_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="null_arg", @@ -58,9 +57,8 @@ ), ] -# --------------------------------------------------------------------------- # Error: unknown fields -# --------------------------------------------------------------------------- +# Property [Unknown Fields]: $map rejects unknown fields in the argument. UNKNOWN_FIELD_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="extra_unknown", @@ -76,9 +74,8 @@ ), ] -# --------------------------------------------------------------------------- # Error: missing required fields -# --------------------------------------------------------------------------- +# Property [Required Fields]: $map requires the input and in fields. MISSING_REQUIRED_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="missing_input", @@ -106,9 +103,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/range/test_range_boundary.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py index 6d9bd538b..fdc250f1f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py @@ -17,9 +17,7 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import INT32_MAX, INT32_MAX_MINUS_1, INT32_MIN -# --------------------------------------------------------------------------- # Success: INT32 boundary values -# --------------------------------------------------------------------------- INT32_BOUNDARY_TESTS: list[RangeTest] = [ RangeTest( id="int32_max_eq", @@ -82,9 +80,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_BOUNDARY_TESTS = INT32_BOUNDARY_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py index 6be425647..836a19ae2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py @@ -18,9 +18,7 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Success: basic ascending ranges (default step=1) -# --------------------------------------------------------------------------- BASIC_ASC_TESTS: list[RangeTest] = [ RangeTest( id="zero_to_five", @@ -59,9 +57,8 @@ ), ] -# --------------------------------------------------------------------------- # Success: custom step -# --------------------------------------------------------------------------- +# Property [Step]: $range respects custom step values. STEP_TESTS: list[RangeTest] = [ RangeTest( id="step_two", @@ -128,9 +125,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: negative step (descending) -# --------------------------------------------------------------------------- NEGATIVE_STEP_TESTS: list[RangeTest] = [ RangeTest( id="descending_basic", @@ -190,9 +185,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: empty results -# --------------------------------------------------------------------------- EMPTY_TESTS: list[RangeTest] = [ RangeTest( id="ascending_wrong_direction", @@ -242,9 +235,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: numeric type acceptance (int64, whole doubles, whole decimal128) -# --------------------------------------------------------------------------- NUMERIC_TYPE_TESTS: list[RangeTest] = [ RangeTest( id="int64_args", @@ -314,9 +305,8 @@ ), ] -# --------------------------------------------------------------------------- # Success: single element result -# --------------------------------------------------------------------------- +# Property [Single Element]: $range produces single-element arrays. SINGLE_ELEMENT_TESTS: list[RangeTest] = [ RangeTest( id="single_element", @@ -351,9 +341,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: negative number ranges -# --------------------------------------------------------------------------- NEGATIVE_RANGE_TESTS: list[RangeTest] = [ RangeTest( id="neg_to_zero", @@ -403,9 +391,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: large range -# --------------------------------------------------------------------------- LARGE_RANGE_TESTS: list[RangeTest] = [ RangeTest( id="large_range", @@ -416,9 +402,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: negative zero start/end -# --------------------------------------------------------------------------- NEG_ZERO_TESTS: list[RangeTest] = [ RangeTest( id="neg_zero_double_start", @@ -450,9 +434,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( BASIC_ASC_TESTS + STEP_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py index 8fa69e9c1..8fc3665ac 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py @@ -43,9 +43,7 @@ INT64_MIN, ) -# --------------------------------------------------------------------------- # Error: non-numeric start -# --------------------------------------------------------------------------- NON_NUMERIC_START_TESTS: list[RangeTest] = [ RangeTest( id="string_start", @@ -133,9 +131,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: non-numeric end -# --------------------------------------------------------------------------- NON_NUMERIC_END_TESTS: list[RangeTest] = [ RangeTest( id="string_end", @@ -223,9 +219,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: non-numeric step -# --------------------------------------------------------------------------- NON_NUMERIC_STEP_TESTS: list[RangeTest] = [ RangeTest( id="string_step", @@ -317,9 +311,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: non-integral start -# --------------------------------------------------------------------------- NON_INTEGRAL_START_TESTS: list[RangeTest] = [ RangeTest( id="fractional_start", @@ -358,9 +350,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: non-integral end -# --------------------------------------------------------------------------- NON_INTEGRAL_END_TESTS: list[RangeTest] = [ RangeTest( id="fractional_end", @@ -392,9 +382,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: non-integral step -# --------------------------------------------------------------------------- NON_INTEGRAL_STEP_TESTS: list[RangeTest] = [ RangeTest( id="fractional_step", @@ -430,9 +418,8 @@ ), ] -# --------------------------------------------------------------------------- # Error: special numeric values -# --------------------------------------------------------------------------- +# Property [Special Numerics]: $map preserves NaN, Infinity, and boundary values. SPECIAL_NUMERIC_TESTS: list[RangeTest] = [ RangeTest( id="nan_start", @@ -492,9 +479,8 @@ ), ] -# --------------------------------------------------------------------------- # Error: step zero → 34449 -# --------------------------------------------------------------------------- +# Property [Step Zero]: $range rejects zero step value. STEP_ZERO_TESTS: list[RangeTest] = [ RangeTest( id="step_zero_int", @@ -546,9 +532,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: out of int32 range -# --------------------------------------------------------------------------- OUT_OF_INT32_TESTS: list[RangeTest] = [ RangeTest( id="start_int64_max", @@ -632,9 +616,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( NON_NUMERIC_START_TESTS + NON_NUMERIC_END_TESTS @@ -682,9 +664,7 @@ def test_range_error_insert(collection, test): ) -# --------------------------------------------------------------------------- # Error: wrong arity -# --------------------------------------------------------------------------- ARITY_ERROR_TESTS = [ pytest.param({"$range": []}, id="zero_args"), pytest.param({"$range": [1]}, id="one_arg"), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py index 38ab8363d..19e201c1c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py @@ -21,9 +21,8 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Field path lookups -# --------------------------------------------------------------------------- +# Property [Field Lookup]: $map resolves field paths in expressions. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_field_path", @@ -55,9 +54,7 @@ ), ] -# --------------------------------------------------------------------------- # $let and system variables -# --------------------------------------------------------------------------- LET_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="let_variable", @@ -94,9 +91,8 @@ ), ] -# --------------------------------------------------------------------------- # Null/missing via expression — $range does NOT propagate null, it errors -# --------------------------------------------------------------------------- +# Property [Null/Missing Fields]: $map handles null and missing field paths. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="missing_start_field", @@ -156,9 +152,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_EXPR_TESTS = FIELD_LOOKUP_TESTS + LET_TESTS + NULL_MISSING_EXPR_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py index f668333f7..cbada117e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py @@ -24,6 +24,7 @@ ) from documentdb_tests.framework.parametrize import pytest_params +# Property [Argument Structure]: $zip rejects malformed arguments. STRUCTURE_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="empty_object", @@ -69,9 +70,8 @@ ), ] -# --------------------------------------------------------------------------- # Non-object argument (error 34460) -# --------------------------------------------------------------------------- +# Property [Object Argument]: $map rejects non-object arguments. NON_OBJECT_ARG_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="int_arg", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py index 480a5ac87..cf193d9e0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py @@ -32,9 +32,8 @@ INT64_MIN, ) -# --------------------------------------------------------------------------- # BSON types preserved after zipping -# --------------------------------------------------------------------------- +# Property [Type Preservation]: $map preserves each element's BSON type. BSON_TYPE_TESTS: list[ZipTest] = [ ZipTest( id="int64_values", @@ -108,9 +107,8 @@ ), ] -# --------------------------------------------------------------------------- # Mixed BSON types across arrays -# --------------------------------------------------------------------------- +# Property [Mixed Types]: $map processes arrays with mixed BSON types. MIXED_BSON_TESTS: list[ZipTest] = [ ZipTest( id="mixed_bson_types", @@ -120,9 +118,8 @@ ), ] -# --------------------------------------------------------------------------- # Special numeric values as elements -# --------------------------------------------------------------------------- +# Property [Special Numerics]: $map preserves NaN, Infinity, and boundary values. SPECIAL_NUMERIC_TESTS: list[ZipTest] = [ ZipTest( id="infinity_values", @@ -156,9 +153,7 @@ ), ] -# --------------------------------------------------------------------------- # Defaults with BSON types -# --------------------------------------------------------------------------- BSON_DEFAULTS_TESTS: list[ZipTest] = [ ZipTest( id="default_int64", @@ -232,9 +227,7 @@ ), ] -# --------------------------------------------------------------------------- # Defaults with special numeric values -# --------------------------------------------------------------------------- SPECIAL_NUMERIC_DEFAULTS_TESTS: list[ZipTest] = [ ZipTest( id="default_infinity", @@ -294,9 +287,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_BSON_TESTS = ( BSON_TYPE_TESTS + MIXED_BSON_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py index a2dd61547..30a1c1891 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py @@ -19,9 +19,8 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Success: basic zipping — equal length arrays -# --------------------------------------------------------------------------- +# Property [Basic Transform]: $map applies an expression to each element. BASIC_TESTS: list[ZipTest] = [ ZipTest( id="two_int_arrays", @@ -55,9 +54,8 @@ ), ] -# --------------------------------------------------------------------------- # Success: unequal length — truncate to shortest (default) -# --------------------------------------------------------------------------- +# Property [Unequal Length]: $zip truncates to the shortest array. UNEQUAL_LENGTH_TESTS: list[ZipTest] = [ ZipTest( id="first_shorter", @@ -79,9 +77,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: useLongestLength -# --------------------------------------------------------------------------- USE_LONGEST_TESTS: list[ZipTest] = [ ZipTest( id="longest_pads_null", @@ -113,9 +109,8 @@ ), ] -# --------------------------------------------------------------------------- # Success: defaults -# --------------------------------------------------------------------------- +# Property [Defaults]: $zip pads shorter arrays with default values. DEFAULTS_TESTS: list[ZipTest] = [ ZipTest( id="defaults_fill_shorter", @@ -191,9 +186,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: empty and single element -# --------------------------------------------------------------------------- DEGENERATE_TESTS: list[ZipTest] = [ ZipTest( id="both_empty", @@ -253,9 +246,8 @@ ), ] -# --------------------------------------------------------------------------- # Success: nested arrays as elements -# --------------------------------------------------------------------------- +# Property [Nested Arrays]: $map operates on nested array structures. NESTED_ARRAY_TESTS: list[ZipTest] = [ ZipTest( id="nested_arrays", @@ -277,9 +269,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: null propagation -# --------------------------------------------------------------------------- NULL_TESTS: list[ZipTest] = [ ZipTest( id="null_first_input", @@ -307,9 +297,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: objects as elements -# --------------------------------------------------------------------------- OBJECT_TESTS: list[ZipTest] = [ ZipTest( id="arrays_of_objects", @@ -319,9 +307,8 @@ ), ] -# --------------------------------------------------------------------------- # Success: large arrays -# --------------------------------------------------------------------------- +# Property [Large Arrays]: $map handles large arrays. LARGE_ARRAY_TESTS: list[ZipTest] = [ ZipTest( id="large_arrays", @@ -331,9 +318,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: many input arrays -# --------------------------------------------------------------------------- MANY_INPUTS_TESTS: list[ZipTest] = [ ZipTest( id="ten_inputs", @@ -349,9 +334,7 @@ ), ] -# --------------------------------------------------------------------------- # Success: multiple arrays of different lengths -# --------------------------------------------------------------------------- MULTI_LENGTH_TESTS: list[ZipTest] = [ ZipTest( id="three_arrays_shortest", @@ -389,9 +372,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( BASIC_TESTS + UNEQUAL_LENGTH_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py index 73d699e8c..f0eae2fa1 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py @@ -43,9 +43,7 @@ INT64_MIN, ) -# --------------------------------------------------------------------------- # Error: non-array input element — standard BSON types -# --------------------------------------------------------------------------- NOT_ARRAY_ELEMENT_TESTS: list[ZipTest] = [ ZipTest( id="string_input", @@ -157,9 +155,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: special float/Decimal128 values as input element -# --------------------------------------------------------------------------- SPECIAL_NUMERIC_ERROR_TESTS: list[ZipTest] = [ ZipTest( id="nan_input", @@ -211,9 +207,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: numeric boundary values as input element -# --------------------------------------------------------------------------- BOUNDARY_ERROR_TESTS: list[ZipTest] = [ ZipTest( id="int32_max_input", @@ -253,9 +247,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: string edge cases as input element -# --------------------------------------------------------------------------- STRING_EDGE_ERROR_TESTS: list[ZipTest] = [ ZipTest( id="comma_separated_string_input", @@ -271,9 +263,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: invalid useLongestLength -# --------------------------------------------------------------------------- USE_LONGEST_ERROR_TESTS: list[ZipTest] = [ ZipTest( id="use_longest_string", @@ -333,9 +323,7 @@ ), ] -# --------------------------------------------------------------------------- # Error: invalid defaults -# --------------------------------------------------------------------------- DEFAULTS_ERROR_TESTS: list[ZipTest] = [ ZipTest( id="defaults_without_use_longest", @@ -394,9 +382,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_INPUT_ELEMENT_TESTS = ( NOT_ARRAY_ELEMENT_TESTS + SPECIAL_NUMERIC_ERROR_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py index a9a0889ff..7d243c952 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py @@ -17,9 +17,8 @@ from documentdb_tests.framework.error_codes import ZIP_REQUIRES_ARRAY_ELEMENT_ERROR from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- # Field path lookups -# --------------------------------------------------------------------------- +# Property [Field Lookup]: $map resolves field paths in expressions. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_field_path", @@ -51,9 +50,7 @@ ), ] -# --------------------------------------------------------------------------- # Composite array paths -# --------------------------------------------------------------------------- COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="composite_array", @@ -71,9 +68,7 @@ ), ] -# --------------------------------------------------------------------------- # $let and system variables -# --------------------------------------------------------------------------- LET_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="let_variable", @@ -103,9 +98,8 @@ ), ] -# --------------------------------------------------------------------------- # Null/missing via expression -# --------------------------------------------------------------------------- +# Property [Null/Missing Fields]: $map handles null and missing field paths. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="missing_field", @@ -181,9 +175,7 @@ ), ] -# --------------------------------------------------------------------------- # Self-composition -# --------------------------------------------------------------------------- SELF_COMPOSITION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_zip_full", @@ -201,9 +193,7 @@ ), ] -# --------------------------------------------------------------------------- # Field path as element in input array -# --------------------------------------------------------------------------- FIELD_PATH_ELEMENT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="field_path_as_element", @@ -230,9 +220,7 @@ ), ] -# --------------------------------------------------------------------------- # Aggregate and test -# --------------------------------------------------------------------------- ALL_EXPR_TESTS = ( FIELD_LOOKUP_TESTS + COMPOSITE_PATH_TESTS From 3fbd66a6e2229dd91adf756da50dc45fa49b9ce2 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 7 Jul 2026 16:58:26 -0700 Subject: [PATCH 05/17] replace zip and range test class with ExpressionTestCase Signed-off-by: Alina (Xi) Li --- .../array/range/test_range_boundary.py | 72 +-- .../array/range/test_range_core_behavior.py | 380 ++++++------- .../array/range/test_range_errors.py | 514 ++++++++---------- .../expressions/array/range/utils/__init__.py | 0 .../array/range/utils/range_common.py | 17 - .../array/zip/test_zip_bson_types.py | 335 +++++++----- .../array/zip/test_zip_core_behavior.py | 360 ++++++------ .../expressions/array/zip/test_zip_errors.py | 323 +++++------ .../expressions/array/zip/utils/__init__.py | 0 .../expressions/array/zip/utils/zip_common.py | 17 - 10 files changed, 992 insertions(+), 1026 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/__init__.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/range_common.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/__init__.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/zip_common.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py index fdc250f1f..5121dff0d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py @@ -6,8 +6,8 @@ import pytest -from documentdb_tests.compatibility.tests.core.operator.expressions.array.range.utils.range_common import ( # noqa: E501 - RangeTest, +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 ( # noqa: E501 assert_expression_result, @@ -18,47 +18,46 @@ from documentdb_tests.framework.test_constants import INT32_MAX, INT32_MAX_MINUS_1, INT32_MIN # Success: INT32 boundary values -INT32_BOUNDARY_TESTS: list[RangeTest] = [ - RangeTest( +INT32_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="int32_max_eq", - start=INT32_MAX, - end=INT32_MAX, + doc={"start": INT32_MAX, "end": INT32_MAX}, + expression={"$range": ["$start", "$end"]}, expected=[], msg="INT32_MAX == INT32_MAX should be empty", ), - RangeTest( + ExpressionTestCase( id="int32_min_eq", - start=INT32_MIN, - end=INT32_MIN, + doc={"start": INT32_MIN, "end": INT32_MIN}, + expression={"$range": ["$start", "$end"]}, expected=[], msg="INT32_MIN == INT32_MIN should be empty", ), - RangeTest( + ExpressionTestCase( id="int32_max_minus1", - start=INT32_MAX_MINUS_1, - end=INT32_MAX, + doc={"start": INT32_MAX_MINUS_1, "end": INT32_MAX}, + expression={"$range": ["$start", "$end"]}, expected=[INT32_MAX_MINUS_1], msg="INT32_MAX-1 to INT32_MAX should produce single element", ), - RangeTest( + ExpressionTestCase( id="int32_min_to_plus3", - start=INT32_MIN, - end=INT32_MIN + 3, + doc={"start": INT32_MIN, "end": INT32_MIN + 3}, + expression={"$range": ["$start", "$end"]}, expected=[INT32_MIN, INT32_MIN + 1, INT32_MIN + 2], msg="INT32_MIN to INT32_MIN+3", ), - RangeTest( + ExpressionTestCase( id="step_int32_max", - start=0, - end=INT32_MAX, - step=INT32_MAX, + doc={"start": 0, "end": INT32_MAX, "step": INT32_MAX}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0], msg="Step INT32_MAX should produce single element", ), - RangeTest( + ExpressionTestCase( id="near_int32_max", - start=INT32_MAX - 7, - end=INT32_MAX, + doc={"start": INT32_MAX - 7, "end": INT32_MAX}, + expression={"$range": ["$start", "$end"]}, expected=[ INT32_MAX - 7, INT32_MAX - 6, @@ -70,11 +69,10 @@ ], msg="Near INT32_MAX range", ), - RangeTest( + ExpressionTestCase( id="cross_int32_boundary", - start=INT32_MIN + 1, - end=INT32_MAX, - step=INT32_MAX, + doc={"start": INT32_MIN + 1, "end": INT32_MAX, "step": INT32_MAX}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[INT32_MIN + 1, 0], msg="Cross INT32 boundary with large step", ), @@ -87,24 +85,10 @@ @pytest.mark.parametrize("test", pytest_params(ALL_BOUNDARY_TESTS)) def test_range_boundary_insert(collection, test): """Test $range boundary values with inserted documents.""" - doc = {"start": test.start, "end": test.end} - args = ["$start", "$end"] - if test.step is not None: - args.append("$step") - doc["step"] = test.step - result = execute_expression_with_insert(collection, {"$range": args}, doc) - assert_expression_result( - result, expected=test.expected, error_code=test.error_code, msg=test.msg - ) - - -@pytest.mark.parametrize("test", pytest_params(ALL_BOUNDARY_TESTS)) -def test_range_boundary_literal(collection, test): - """Test $range boundary values with literal values.""" - args = [test.start, test.end] - if test.step is not None: - args.append(test.step) - result = execute_expression(collection, {"$range": args}) + 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/range/test_range_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py index 836a19ae2..2cbb17fc7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py @@ -8,8 +8,8 @@ import pytest from bson import Decimal128, Int64 -from documentdb_tests.compatibility.tests.core.operator.expressions.array.range.utils.range_common import ( # noqa: E501 - RangeTest, +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 ( # noqa: E501 assert_expression_result, @@ -19,39 +19,39 @@ from documentdb_tests.framework.parametrize import pytest_params # Success: basic ascending ranges (default step=1) -BASIC_ASC_TESTS: list[RangeTest] = [ - RangeTest( +BASIC_ASC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="zero_to_five", - start=0, - end=5, + doc={"start": 0, "end": 5}, + expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2, 3, 4], msg="Should generate 0..4", ), - RangeTest( + ExpressionTestCase( id="one_to_four", - start=1, - end=4, + doc={"start": 1, "end": 4}, + expression={"$range": ["$start", "$end"]}, expected=[1, 2, 3], msg="Should generate 1..3", ), - RangeTest( + ExpressionTestCase( id="negative_range", - start=-5, - end=-1, + doc={"start": -5, "end": -1}, + expression={"$range": ["$start", "$end"]}, expected=[-5, -4, -3, -2], msg="Should generate -5..-2", ), - RangeTest( + ExpressionTestCase( id="start_equals_end", - start=5, - end=5, + doc={"start": 5, "end": 5}, + expression={"$range": ["$start", "$end"]}, expected=[], msg="Should return empty when start equals end", ), - RangeTest( + ExpressionTestCase( id="start_greater_than_end", - start=10, - end=5, + doc={"start": 10, "end": 5}, + expression={"$range": ["$start", "$end"]}, expected=[], msg="Should return empty when start > end with default step", ), @@ -59,247 +59,226 @@ # Success: custom step # Property [Step]: $range respects custom step values. -STEP_TESTS: list[RangeTest] = [ - RangeTest( +STEP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="step_two", - start=0, - end=10, - step=2, + doc={"start": 0, "end": 10, "step": 2}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 2, 4, 6, 8], msg="Should generate with step 2", ), - RangeTest( + ExpressionTestCase( id="step_three", - start=0, - end=10, - step=3, + doc={"start": 0, "end": 10, "step": 3}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 3, 6, 9], msg="Should generate with step 3", ), - RangeTest( + ExpressionTestCase( id="step_five", - start=0, - end=20, - step=5, + doc={"start": 0, "end": 20, "step": 5}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 5, 10, 15], msg="Should generate with step 5", ), - RangeTest( + ExpressionTestCase( id="step_one_explicit", - start=0, - end=3, - step=1, + doc={"start": 0, "end": 3, "step": 1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 1, 2], msg="Explicit step 1 same as default", ), - RangeTest( + ExpressionTestCase( id="step_overshoots", - start=0, - end=5, - step=3, + doc={"start": 0, "end": 5, "step": 3}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 3], msg="Should stop when step overshoots end", ), - RangeTest( + ExpressionTestCase( id="step_exactly_reaches", - start=0, - end=6, - step=2, + doc={"start": 0, "end": 6, "step": 2}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 2, 4], msg="End is exclusive even when step exactly reaches it", ), - RangeTest( + ExpressionTestCase( id="start_nonzero", - start=5, - end=15, + doc={"start": 5, "end": 15}, + expression={"$range": ["$start", "$end"]}, expected=[5, 6, 7, 8, 9, 10, 11, 12, 13, 14], msg="Should work with nonzero start", ), - RangeTest( + ExpressionTestCase( id="step_4", - start=5, - end=15, - step=4, + doc={"start": 5, "end": 15, "step": 4}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[5, 9, 13], msg="Should work with step 4", ), ] # Success: negative step (descending) -NEGATIVE_STEP_TESTS: list[RangeTest] = [ - RangeTest( +NEGATIVE_STEP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="descending_basic", - start=5, - end=0, - step=-1, + doc={"start": 5, "end": 0, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[5, 4, 3, 2, 1], msg="Should generate descending 5..1", ), - RangeTest( + ExpressionTestCase( id="descending_step_two", - start=10, - end=0, - step=-2, + doc={"start": 10, "end": 0, "step": -2}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[10, 8, 6, 4, 2], msg="Should generate descending with step -2", ), - RangeTest( + ExpressionTestCase( id="descending_negative_range", - start=-1, - end=-5, - step=-1, + doc={"start": -1, "end": -5, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[-1, -2, -3, -4], msg="Should generate descending in negative range", ), - RangeTest( + ExpressionTestCase( id="descending_start_equals_end", - start=5, - end=5, - step=-1, + doc={"start": 5, "end": 5, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[], msg="Should return empty when start equals end with negative step", ), - RangeTest( + ExpressionTestCase( id="descending_wrong_direction", - start=0, - end=5, - step=-1, + doc={"start": 0, "end": 5, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[], msg="Should return empty when step direction mismatches", ), - RangeTest( + ExpressionTestCase( id="descending_step_neg3", - start=10, - end=0, - step=-3, + doc={"start": 10, "end": 0, "step": -3}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[10, 7, 4, 1], msg="Should generate descending with step -3", ), - RangeTest( + ExpressionTestCase( id="descending_past_zero", - start=5, - end=-1, - step=-1, + doc={"start": 5, "end": -1, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[5, 4, 3, 2, 1, 0], msg="Should descend past zero", ), ] # Success: empty results -EMPTY_TESTS: list[RangeTest] = [ - RangeTest( +EMPTY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="ascending_wrong_direction", - start=5, - end=0, - step=1, + doc={"start": 5, "end": 0, "step": 1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[], msg="Should return empty when ascending step but start > end", ), - RangeTest( + ExpressionTestCase( id="zero_zero_pos_step", - start=0, - end=0, - step=1, + doc={"start": 0, "end": 0, "step": 1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[], msg="0 to 0 step 1 should be empty", ), - RangeTest( + ExpressionTestCase( id="zero_zero_neg_step", - start=0, - end=0, - step=-1, + doc={"start": 0, "end": 0, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[], msg="0 to 0 step -1 should be empty", ), - RangeTest( + ExpressionTestCase( id="neg_equal", - start=-1, - end=-1, + doc={"start": -1, "end": -1}, + expression={"$range": ["$start", "$end"]}, expected=[], msg="-1 to -1 should be empty", ), - RangeTest( + ExpressionTestCase( id="large_equal", - start=1000000, - end=1000000, + doc={"start": 1000000, "end": 1000000}, + expression={"$range": ["$start", "$end"]}, expected=[], msg="Large equal start/end should be empty", ), - RangeTest( + ExpressionTestCase( id="neg_mismatch", - start=-1, - end=-5, - step=1, + doc={"start": -1, "end": -5, "step": 1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[], msg="Negative range with positive step should be empty", ), ] # Success: numeric type acceptance (int64, whole doubles, whole decimal128) -NUMERIC_TYPE_TESTS: list[RangeTest] = [ - RangeTest( +NUMERIC_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="int64_args", - start=Int64(0), - end=Int64(3), + doc={"start": Int64(0), "end": Int64(3)}, + expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], msg="Should accept Int64 arguments", ), - RangeTest( + ExpressionTestCase( id="whole_double_args", - start=0.0, - end=3.0, + doc={"start": 0.0, "end": 3.0}, + expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], msg="Should accept whole-number double arguments", ), - RangeTest( + ExpressionTestCase( id="whole_decimal128_args", - start=Decimal128("0"), - end=Decimal128("3"), + doc={"start": Decimal128("0"), "end": Decimal128("3")}, + expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], msg="Should accept whole-number Decimal128 arguments", ), - RangeTest( + ExpressionTestCase( id="int64_step", - start=0, - end=6, - step=Int64(2), + doc={"start": 0, "end": 6, "step": Int64(2)}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 2, 4], msg="Should accept Int64 step", ), - RangeTest( + ExpressionTestCase( id="whole_double_step", - start=0, - end=6, - step=2.0, + doc={"start": 0, "end": 6, "step": 2.0}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 2, 4], msg="Should accept whole-number double step", ), - RangeTest( + ExpressionTestCase( id="whole_decimal128_step", - start=0, - end=6, - step=Decimal128("2"), + doc={"start": 0, "end": 6, "step": Decimal128("2")}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 2, 4], msg="Should accept whole-number Decimal128 step", ), - RangeTest( + ExpressionTestCase( id="decimal128_trailing_zero_start", - start=Decimal128("0.0"), - end=3, + doc={"start": Decimal128("0.0"), "end": 3}, + expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], msg="Should accept Decimal128 0.0 as start", ), - RangeTest( + ExpressionTestCase( id="decimal128_trailing_zero_end", - start=0, - end=Decimal128("3.0"), + doc={"start": 0, "end": Decimal128("3.0")}, + expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], msg="Should accept Decimal128 3.0 as end", ), - RangeTest( + ExpressionTestCase( id="decimal128_trailing_zero_both", - start=Decimal128("0.0"), - end=Decimal128("3.0"), + doc={"start": Decimal128("0.0"), "end": Decimal128("3.0")}, + expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], msg="Should accept Decimal128 0.0 start and 3.0 end", ), @@ -307,128 +286,121 @@ # Success: single element result # Property [Single Element]: $range produces single-element arrays. -SINGLE_ELEMENT_TESTS: list[RangeTest] = [ - RangeTest( +SINGLE_ELEMENT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="single_element", - start=0, - end=1, + doc={"start": 0, "end": 1}, + expression={"$range": ["$start", "$end"]}, expected=[0], msg="Should return single element", ), - RangeTest( + ExpressionTestCase( id="desc_single", - start=1, - end=0, - step=-1, + doc={"start": 1, "end": 0, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[1], msg="Descending single element", ), - RangeTest( + ExpressionTestCase( id="step_eq_range", - start=0, - end=5, - step=5, + doc={"start": 0, "end": 5, "step": 5}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0], msg="Step equal to range should produce single element", ), - RangeTest( + ExpressionTestCase( id="step_gt_range", - start=0, - end=5, - step=10, + doc={"start": 0, "end": 5, "step": 10}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0], msg="Step greater than range should produce single element", ), ] # Success: negative number ranges -NEGATIVE_RANGE_TESTS: list[RangeTest] = [ - RangeTest( +NEGATIVE_RANGE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="neg_to_zero", - start=-5, - end=0, + doc={"start": -5, "end": 0}, + expression={"$range": ["$start", "$end"]}, expected=[-5, -4, -3, -2, -1], msg="Negative start to zero ascending", ), - RangeTest( + ExpressionTestCase( id="zero_to_neg", - start=0, - end=-5, - step=-1, + doc={"start": 0, "end": -5, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[0, -1, -2, -3, -4], msg="Zero to negative descending", ), - RangeTest( + ExpressionTestCase( id="cross_zero_asc", - start=-3, - end=3, + doc={"start": -3, "end": 3}, + expression={"$range": ["$start", "$end"]}, expected=[-3, -2, -1, 0, 1, 2], msg="Crossing zero ascending", ), - RangeTest( + ExpressionTestCase( id="cross_zero_desc", - start=3, - end=-3, - step=-1, + doc={"start": 3, "end": -3, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[3, 2, 1, 0, -1, -2], msg="Crossing zero descending", ), - RangeTest( + ExpressionTestCase( id="neg_desc_step2", - start=-10, - end=-20, - step=-2, + doc={"start": -10, "end": -20, "step": -2}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[-10, -12, -14, -16, -18], msg="Negative descending with step -2", ), - RangeTest( + ExpressionTestCase( id="neg_asc_step2", - start=-10, - end=-1, - step=2, + doc={"start": -10, "end": -1, "step": 2}, + expression={"$range": ["$start", "$end", "$step"]}, expected=[-10, -8, -6, -4, -2], msg="Negative ascending with step 2", ), ] # Success: large range -LARGE_RANGE_TESTS: list[RangeTest] = [ - RangeTest( +LARGE_RANGE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="large_range", - start=0, - end=1000, + doc={"start": 0, "end": 1000}, + expression={"$range": ["$start", "$end"]}, expected=list(range(1000)), msg="Should generate large range", ), ] # Success: negative zero start/end -NEG_ZERO_TESTS: list[RangeTest] = [ - RangeTest( +NEG_ZERO_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="neg_zero_double_start", - start=-0.0, - end=5, + doc={"start": -0.0, "end": 5}, + expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2, 3, 4], msg="Negative zero double start treated as 0", ), - RangeTest( + ExpressionTestCase( id="neg_zero_decimal_start", - start=Decimal128("-0"), - end=5, + doc={"start": Decimal128("-0"), "end": 5}, + expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2, 3, 4], msg="Negative zero Decimal128 start treated as 0", ), - RangeTest( + ExpressionTestCase( id="neg_zero_double_end", - start=0, - end=-0.0, + doc={"start": 0, "end": -0.0}, + expression={"$range": ["$start", "$end"]}, expected=[], msg="Negative zero double end treated as 0", ), - RangeTest( + ExpressionTestCase( id="neg_zero_decimal_end", - start=0, - end=Decimal128("-0"), + doc={"start": 0, "end": Decimal128("-0")}, + expression={"$range": ["$start", "$end"]}, expected=[], msg="Negative zero Decimal128 end treated as 0", ), @@ -451,32 +423,10 @@ @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) def test_range_insert(collection, test): """Test $range with values from inserted documents.""" - doc = {"start": test.start, "end": test.end} - args = ["$start", "$end"] - if test.step is not None: - args.append("$step") - doc["step"] = test.step - result = execute_expression_with_insert(collection, {"$range": args}, doc) - assert_expression_result( - result, expected=test.expected, error_code=test.error_code, msg=test.msg - ) - - -TEST_SUBSET_FOR_LITERAL = [ - BASIC_ASC_TESTS[0], # zero_to_five - STEP_TESTS[0], # step_two - NEGATIVE_STEP_TESTS[0], # descending_basic - EMPTY_TESTS[0], # ascending_wrong_direction -] - - -@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) -def test_range_literal(collection, test): - """Test $range with literal values.""" - args = [test.start, test.end] - if test.step is not None: - args.append(test.step) - result = execute_expression(collection, {"$range": args}) + 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/range/test_range_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py index 8fc3665ac..18ca191c7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.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.range.utils.range_common import ( # noqa: E501 - RangeTest, +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 ( # noqa: E501 assert_expression_result, @@ -44,375 +44,360 @@ ) # Error: non-numeric start -NON_NUMERIC_START_TESTS: list[RangeTest] = [ - RangeTest( +NON_NUMERIC_START_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="string_start", - start="hello", - end=5, + doc={"start": "hello", "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject string start", ), - RangeTest( + ExpressionTestCase( id="bool_start", - start=True, - end=5, + doc={"start": True, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject bool start", ), - RangeTest( + ExpressionTestCase( id="null_start", - start=None, - end=5, + doc={"start": None, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject null start", ), - RangeTest( + ExpressionTestCase( id="object_start", - start={"a": 1}, - end=5, + doc={"start": {"a": 1}, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject object start", ), - RangeTest( + ExpressionTestCase( id="array_start", - start=[1], - end=5, + doc={"start": [1], "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject array start", ), - RangeTest( + ExpressionTestCase( id="objectid_start", - start=ObjectId(), - end=5, + doc={"start": ObjectId(), "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject objectid start", ), - RangeTest( + ExpressionTestCase( id="datetime_start", - start=datetime(2024, 1, 1, tzinfo=timezone.utc), - end=5, + doc={"start": datetime(2024, 1, 1, tzinfo=timezone.utc), "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject datetime start", ), - RangeTest( + ExpressionTestCase( id="binary_start", - start=Binary(b"x", 0), - end=5, + doc={"start": Binary(b"x", 0), "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject binary start", ), - RangeTest( + ExpressionTestCase( id="regex_start", - start=Regex("x"), - end=5, + doc={"start": Regex("x"), "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject regex start", ), - RangeTest( + ExpressionTestCase( id="maxkey_start", - start=MaxKey(), - end=5, + doc={"start": MaxKey(), "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject maxkey start", ), - RangeTest( + ExpressionTestCase( id="minkey_start", - start=MinKey(), - end=5, + doc={"start": MinKey(), "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject minkey start", ), - RangeTest( + ExpressionTestCase( id="timestamp_start", - start=Timestamp(0, 0), - end=5, + doc={"start": Timestamp(0, 0), "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, msg="Should reject timestamp start", ), ] # Error: non-numeric end -NON_NUMERIC_END_TESTS: list[RangeTest] = [ - RangeTest( +NON_NUMERIC_END_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="string_end", - start=0, - end="hello", + doc={"start": 0, "end": "hello"}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject string end", ), - RangeTest( + ExpressionTestCase( id="bool_end", - start=0, - end=True, + doc={"start": 0, "end": True}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject bool end", ), - RangeTest( + ExpressionTestCase( id="null_end", - start=0, - end=None, + doc={"start": 0, "end": None}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject null end", ), - RangeTest( + ExpressionTestCase( id="object_end", - start=0, - end={"a": 1}, + doc={"start": 0, "end": {"a": 1}}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject object end", ), - RangeTest( + ExpressionTestCase( id="array_end", - start=0, - end=[1], + doc={"start": 0, "end": [1]}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject array end", ), - RangeTest( + ExpressionTestCase( id="objectid_end", - start=0, - end=ObjectId(), + doc={"start": 0, "end": ObjectId()}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject objectid end", ), - RangeTest( + ExpressionTestCase( id="datetime_end", - start=0, - end=datetime(2024, 1, 1, tzinfo=timezone.utc), + doc={"start": 0, "end": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject datetime end", ), - RangeTest( + ExpressionTestCase( id="binary_end", - start=0, - end=Binary(b"x", 0), + doc={"start": 0, "end": Binary(b"x", 0)}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject binary end", ), - RangeTest( + ExpressionTestCase( id="regex_end", - start=0, - end=Regex("x"), + doc={"start": 0, "end": Regex("x")}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject regex end", ), - RangeTest( + ExpressionTestCase( id="maxkey_end", - start=0, - end=MaxKey(), + doc={"start": 0, "end": MaxKey()}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject maxkey end", ), - RangeTest( + ExpressionTestCase( id="minkey_end", - start=0, - end=MinKey(), + doc={"start": 0, "end": MinKey()}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject minkey end", ), - RangeTest( + ExpressionTestCase( id="timestamp_end", - start=0, - end=Timestamp(0, 0), + doc={"start": 0, "end": Timestamp(0, 0)}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, msg="Should reject timestamp end", ), ] # Error: non-numeric step -NON_NUMERIC_STEP_TESTS: list[RangeTest] = [ - RangeTest( +NON_NUMERIC_STEP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="string_step", - start=0, - end=5, - step="bad", + doc={"start": 0, "end": 5, "step": "bad"}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject string step", ), - RangeTest( + ExpressionTestCase( id="bool_step", - start=0, - end=5, - step=True, + doc={"start": 0, "end": 5, "step": True}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject bool step", ), - RangeTest( + ExpressionTestCase( id="object_step", - start=0, - end=5, - step={"a": 1}, + doc={"start": 0, "end": 5, "step": {"a": 1}}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject object step", ), - RangeTest( + ExpressionTestCase( id="array_step", - start=0, - end=5, - step=[1], + doc={"start": 0, "end": 5, "step": [1]}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject array step", ), - RangeTest( + ExpressionTestCase( id="objectid_step", - start=0, - end=5, - step=ObjectId(), + doc={"start": 0, "end": 5, "step": ObjectId()}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject objectid step", ), - RangeTest( + ExpressionTestCase( id="datetime_step", - start=0, - end=5, - step=datetime(2024, 1, 1, tzinfo=timezone.utc), + doc={"start": 0, "end": 5, "step": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject datetime step", ), - RangeTest( + ExpressionTestCase( id="binary_step", - start=0, - end=5, - step=Binary(b"x", 0), + doc={"start": 0, "end": 5, "step": Binary(b"x", 0)}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject binary step", ), - RangeTest( + ExpressionTestCase( id="regex_step", - start=0, - end=5, - step=Regex("x"), + doc={"start": 0, "end": 5, "step": Regex("x")}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject regex step", ), - RangeTest( + ExpressionTestCase( id="maxkey_step", - start=0, - end=5, - step=MaxKey(), + doc={"start": 0, "end": 5, "step": MaxKey()}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject maxkey step", ), - RangeTest( + ExpressionTestCase( id="minkey_step", - start=0, - end=5, - step=MinKey(), + doc={"start": 0, "end": 5, "step": MinKey()}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject minkey step", ), - RangeTest( + ExpressionTestCase( id="timestamp_step", - start=0, - end=5, - step=Timestamp(0, 0), + doc={"start": 0, "end": 5, "step": Timestamp(0, 0)}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, msg="Should reject timestamp step", ), ] # Error: non-integral start -NON_INTEGRAL_START_TESTS: list[RangeTest] = [ - RangeTest( +NON_INTEGRAL_START_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="fractional_start", - start=1.5, - end=5, + doc={"start": 1.5, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject fractional start", ), - RangeTest( + ExpressionTestCase( id="decimal128_fractional_start", - start=Decimal128("0.5"), - end=5, + doc={"start": Decimal128("0.5"), "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject fractional Decimal128 start", ), - RangeTest( + ExpressionTestCase( id="negative_fractional_start", - start=-1.5, - end=5, + doc={"start": -1.5, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject negative fractional start", ), - RangeTest( + ExpressionTestCase( id="decimal128_negative_fractional_start", - start=Decimal128("-1.5"), - end=5, + doc={"start": Decimal128("-1.5"), "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject negative fractional Decimal128 start", ), - RangeTest( + ExpressionTestCase( id="decimal128_negative_nan_start", - start=Decimal128("-NaN"), - end=5, + doc={"start": Decimal128("-NaN"), "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject Decimal128 -NaN start", ), ] # Error: non-integral end -NON_INTEGRAL_END_TESTS: list[RangeTest] = [ - RangeTest( +NON_INTEGRAL_END_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="fractional_end", - start=0, - end=5.5, + doc={"start": 0, "end": 5.5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="Should reject fractional end", ), - RangeTest( + ExpressionTestCase( id="decimal128_fractional_end", - start=0, - end=Decimal128("5.5"), + doc={"start": 0, "end": Decimal128("5.5")}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="Should reject fractional Decimal128 end", ), - RangeTest( + ExpressionTestCase( id="negative_fractional_end", - start=0, - end=-1.5, + doc={"start": 0, "end": -1.5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="Should reject negative fractional end", ), - RangeTest( + ExpressionTestCase( id="decimal128_negative_fractional_end", - start=0, - end=Decimal128("-1.5"), + doc={"start": 0, "end": Decimal128("-1.5")}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="Should reject negative fractional Decimal128 end", ), ] # Error: non-integral step -NON_INTEGRAL_STEP_TESTS: list[RangeTest] = [ - RangeTest( +NON_INTEGRAL_STEP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="fractional_step", - start=0, - end=10, - step=1.5, + doc={"start": 0, "end": 10, "step": 1.5}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, msg="Should reject fractional step", ), - RangeTest( + ExpressionTestCase( id="decimal128_fractional_step", - start=0, - end=10, - step=Decimal128("1.5"), + doc={"start": 0, "end": 10, "step": Decimal128("1.5")}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, msg="Should reject fractional Decimal128 step", ), - RangeTest( + ExpressionTestCase( id="negative_fractional_step", - start=10, - end=0, - step=-1.5, + doc={"start": 10, "end": 0, "step": -1.5}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, msg="Should reject negative fractional step", ), - RangeTest( + ExpressionTestCase( id="decimal128_negative_fractional_step", - start=10, - end=0, - step=Decimal128("-1.5"), + doc={"start": 10, "end": 0, "step": Decimal128("-1.5")}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, msg="Should reject negative fractional Decimal128 step", ), @@ -420,60 +405,60 @@ # Error: special numeric values # Property [Special Numerics]: $map preserves NaN, Infinity, and boundary values. -SPECIAL_NUMERIC_TESTS: list[RangeTest] = [ - RangeTest( +SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="nan_start", - start=FLOAT_NAN, - end=5, + doc={"start": FLOAT_NAN, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject NaN start", ), - RangeTest( + ExpressionTestCase( id="inf_start", - start=FLOAT_INFINITY, - end=5, + doc={"start": FLOAT_INFINITY, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject Infinity start", ), - RangeTest( + ExpressionTestCase( id="neg_inf_start", - start=FLOAT_NEGATIVE_INFINITY, - end=5, + doc={"start": FLOAT_NEGATIVE_INFINITY, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject -Infinity start", ), - RangeTest( + ExpressionTestCase( id="nan_end", - start=0, - end=FLOAT_NAN, + doc={"start": 0, "end": FLOAT_NAN}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="Should reject NaN end", ), - RangeTest( + ExpressionTestCase( id="inf_end", - start=0, - end=FLOAT_INFINITY, + doc={"start": 0, "end": FLOAT_INFINITY}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="Should reject Infinity end", ), - RangeTest( + ExpressionTestCase( id="decimal128_nan_start", - start=DECIMAL128_NAN, - end=5, + doc={"start": DECIMAL128_NAN, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject Decimal128 NaN start", ), - RangeTest( + ExpressionTestCase( id="decimal128_inf_start", - start=DECIMAL128_INFINITY, - end=5, + doc={"start": DECIMAL128_INFINITY, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject Decimal128 Infinity start", ), - RangeTest( + ExpressionTestCase( id="decimal128_neg_inf_end", - start=0, - end=DECIMAL128_NEGATIVE_INFINITY, + doc={"start": 0, "end": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="Should reject Decimal128 -Infinity end", ), @@ -481,136 +466,127 @@ # Error: step zero → 34449 # Property [Step Zero]: $range rejects zero step value. -STEP_ZERO_TESTS: list[RangeTest] = [ - RangeTest( +STEP_ZERO_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="step_zero_int", - start=0, - end=5, - step=0, + doc={"start": 0, "end": 5, "step": 0}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, msg="Should reject step 0", ), - RangeTest( + ExpressionTestCase( id="step_zero_int64", - start=0, - end=5, - step=Int64(0), + doc={"start": 0, "end": 5, "step": Int64(0)}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, msg="Should reject Int64 step 0", ), - RangeTest( + ExpressionTestCase( id="step_zero_double", - start=0, - end=5, - step=0.0, + doc={"start": 0, "end": 5, "step": 0.0}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, msg="Should reject double step 0.0", ), - RangeTest( + ExpressionTestCase( id="step_zero_decimal128", - start=0, - end=5, - step=Decimal128("0"), + doc={"start": 0, "end": 5, "step": Decimal128("0")}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, msg="Should reject Decimal128 step 0", ), - RangeTest( + ExpressionTestCase( id="step_neg_zero_double", - start=0, - end=5, - step=-0.0, + doc={"start": 0, "end": 5, "step": -0.0}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, msg="Should reject negative zero double step", ), - RangeTest( + ExpressionTestCase( id="step_neg_zero_decimal128", - start=0, - end=5, - step=Decimal128("-0"), + doc={"start": 0, "end": 5, "step": Decimal128("-0")}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, msg="Should reject negative zero Decimal128 step", ), ] # Error: out of int32 range -OUT_OF_INT32_TESTS: list[RangeTest] = [ - RangeTest( +OUT_OF_INT32_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="start_int64_max", - start=INT64_MAX, - end=5, + doc={"start": INT64_MAX, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject INT64_MAX start", ), - RangeTest( + ExpressionTestCase( id="start_int64_min", - start=INT64_MIN, - end=5, + doc={"start": INT64_MIN, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject INT64_MIN start", ), - RangeTest( + ExpressionTestCase( id="end_int64_max", - start=0, - end=INT64_MAX, + doc={"start": 0, "end": INT64_MAX}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="Should reject INT64_MAX end", ), - RangeTest( + ExpressionTestCase( id="end_int64_min", - start=0, - end=INT64_MIN, + doc={"start": 0, "end": INT64_MIN}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="Should reject INT64_MIN end", ), - RangeTest( + ExpressionTestCase( id="step_int64_max", - start=0, - end=5, - step=INT64_MAX, + doc={"start": 0, "end": 5, "step": INT64_MAX}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, msg="Should reject INT64_MAX step", ), - RangeTest( + ExpressionTestCase( id="start_int32_overflow", - start=INT32_OVERFLOW, - end=5, + doc={"start": INT32_OVERFLOW, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject INT32_OVERFLOW start", ), - RangeTest( + ExpressionTestCase( id="start_int32_underflow", - start=INT32_UNDERFLOW, - end=5, + doc={"start": INT32_UNDERFLOW, "end": 5}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="Should reject INT32_UNDERFLOW start", ), - RangeTest( + ExpressionTestCase( id="end_int32_overflow", - start=0, - end=INT32_OVERFLOW, + doc={"start": 0, "end": INT32_OVERFLOW}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="Should reject INT32_OVERFLOW end", ), - RangeTest( + ExpressionTestCase( id="end_int32_underflow", - start=0, - end=INT32_UNDERFLOW, + doc={"start": 0, "end": INT32_UNDERFLOW}, + expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="Should reject INT32_UNDERFLOW end", ), - RangeTest( + ExpressionTestCase( id="step_int32_overflow", - start=0, - end=5, - step=INT32_OVERFLOW, + doc={"start": 0, "end": 5, "step": INT32_OVERFLOW}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, msg="Should reject INT32_OVERFLOW step", ), - RangeTest( + ExpressionTestCase( id="step_int32_underflow", - start=0, - end=5, - step=INT32_UNDERFLOW, + doc={"start": 0, "end": 5, "step": INT32_UNDERFLOW}, + expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, msg="Should reject INT32_UNDERFLOW step", ), @@ -630,35 +606,13 @@ ) -TEST_SUBSET_FOR_LITERAL = [ - NON_NUMERIC_START_TESTS[0], # string_start - NON_INTEGRAL_START_TESTS[0], # fractional_start - SPECIAL_NUMERIC_TESTS[0], # nan_start - STEP_ZERO_TESTS[0], # step_zero_int -] - - -@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) -def test_range_error_literal(collection, test): - """Test $range error with literal values.""" - args = [test.start, test.end] - if test.step is not None: - args.append(test.step) - result = execute_expression(collection, {"$range": args}) - 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_range_error_insert(collection, test): """Test $range error with values from inserted documents.""" - doc = {"start": test.start, "end": test.end} - args = ["$start", "$end"] - if test.step is not None: - args.append("$step") - doc["step"] = test.step - result = execute_expression_with_insert(collection, {"$range": args}, doc) + 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/range/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/range_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/range_common.py deleted file mode 100644 index 43b2c2d5b..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/utils/range_common.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Shared test infrastructure for $range expression tests. -""" - -from dataclasses import dataclass -from typing import Any - -from documentdb_tests.framework.test_case import BaseTestCase - - -@dataclass(frozen=True) -class RangeTest(BaseTestCase): - """Test case for $range operator.""" - - start: Any = None - end: Any = None - step: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py index cf193d9e0..e3d972ff3 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_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.zip.utils.zip_common import ( # noqa: E501 - ZipTest, +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 ( # noqa: E501 assert_expression_result, @@ -34,69 +34,78 @@ # BSON types preserved after zipping # Property [Type Preservation]: $map preserves each element's BSON type. -BSON_TYPE_TESTS: list[ZipTest] = [ - ZipTest( +BSON_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="int64_values", - inputs=[[Int64(1)], [Int64(2)]], + doc={"arr0": [Int64(1)], "arr1": [Int64(2)]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[Int64(1), Int64(2)]], msg="Should preserve Int64 values", ), - ZipTest( + ExpressionTestCase( id="decimal128_values", - inputs=[[Decimal128("1.5")], [Decimal128("2.5")]], + doc={"arr0": [Decimal128("1.5")], "arr1": [Decimal128("2.5")]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[Decimal128("1.5"), Decimal128("2.5")]], msg="Should preserve Decimal128 values", ), - ZipTest( + ExpressionTestCase( id="datetime_values", - inputs=[ - [datetime(2024, 1, 1, tzinfo=timezone.utc)], - [datetime(2024, 6, 1, tzinfo=timezone.utc)], - ], + doc={ + "arr0": [datetime(2024, 1, 1, tzinfo=timezone.utc)], + "arr1": [datetime(2024, 6, 1, tzinfo=timezone.utc)], + }, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[ [datetime(2024, 1, 1, tzinfo=timezone.utc), datetime(2024, 6, 1, tzinfo=timezone.utc)] ], msg="Should preserve datetime values", ), - ZipTest( + ExpressionTestCase( id="objectid_values", - inputs=[ - [ObjectId("000000000000000000000001")], - [ObjectId("000000000000000000000002")], - ], + doc={ + "arr0": [ObjectId("000000000000000000000001")], + "arr1": [ObjectId("000000000000000000000002")], + }, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")]], msg="Should preserve ObjectId values", ), - ZipTest( + ExpressionTestCase( id="binary_values", - inputs=[[Binary(b"\x01", 0)], [Binary(b"\x02", 0)]], + doc={"arr0": [Binary(b"\x01", 0)], "arr1": [Binary(b"\x02", 0)]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[b"\x01", b"\x02"]], msg="Should preserve Binary values", ), - ZipTest( + ExpressionTestCase( id="regex_values", - inputs=[[Regex("^a", "i")], [Regex("^b", "i")]], + doc={"arr0": [Regex("^a", "i")], "arr1": [Regex("^b", "i")]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[Regex("^a", "i"), Regex("^b", "i")]], msg="Should preserve Regex values", ), - ZipTest( + ExpressionTestCase( id="timestamp_values", - inputs=[[Timestamp(1, 0)], [Timestamp(2, 0)]], + doc={"arr0": [Timestamp(1, 0)], "arr1": [Timestamp(2, 0)]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[Timestamp(1, 0), Timestamp(2, 0)]], msg="Should preserve Timestamp values", ), - ZipTest( + ExpressionTestCase( id="minkey_maxkey", - inputs=[[MinKey()], [MaxKey()]], + doc={"arr0": [MinKey()], "arr1": [MaxKey()]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[MinKey(), MaxKey()]], msg="Should preserve MinKey/MaxKey values", ), - ZipTest( + ExpressionTestCase( id="uuid_values", - inputs=[ - [Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))], - [Binary.from_uuid(UUID("fedcba98-7654-3210-0123-456789abcdef"))], - ], + doc={ + "arr0": [Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))], + "arr1": [Binary.from_uuid(UUID("fedcba98-7654-3210-0123-456789abcdef"))], + }, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[ [ Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210")), @@ -109,10 +118,11 @@ # Mixed BSON types across arrays # Property [Mixed Types]: $map processes arrays with mixed BSON types. -MIXED_BSON_TESTS: list[ZipTest] = [ - ZipTest( +MIXED_BSON_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="mixed_bson_types", - inputs=[[1, "two", Int64(3)], [Decimal128("4"), True, MinKey()]], + doc={"arr0": [1, "two", Int64(3)], "arr1": [Decimal128("4"), True, MinKey()]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, Decimal128("4")], ["two", True], [Int64(3), MinKey()]], msg="Should zip mixed BSON types preserving each", ), @@ -120,168 +130,248 @@ # Special numeric values as elements # Property [Special Numerics]: $map preserves NaN, Infinity, and boundary values. -SPECIAL_NUMERIC_TESTS: list[ZipTest] = [ - ZipTest( +SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="infinity_values", - inputs=[[FLOAT_INFINITY], [FLOAT_NEGATIVE_INFINITY]], + doc={"arr0": [FLOAT_INFINITY], "arr1": [FLOAT_NEGATIVE_INFINITY]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY]], msg="Should preserve infinity values", ), - ZipTest( + ExpressionTestCase( id="decimal128_infinity", - inputs=[[DECIMAL128_INFINITY], [DECIMAL128_NEGATIVE_INFINITY]], + doc={"arr0": [DECIMAL128_INFINITY], "arr1": [DECIMAL128_NEGATIVE_INFINITY]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY]], msg="Should preserve Decimal128 infinity values", ), - ZipTest( + ExpressionTestCase( id="boundary_values", - inputs=[[INT32_MIN, INT32_MAX], [INT64_MIN, INT64_MAX]], + doc={"arr0": [INT32_MIN, INT32_MAX], "arr1": [INT64_MIN, INT64_MAX]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[INT32_MIN, INT64_MIN], [INT32_MAX, INT64_MAX]], msg="Should preserve numeric boundary values", ), - ZipTest( + ExpressionTestCase( id="negative_zero", - inputs=[[DOUBLE_NEGATIVE_ZERO], [0]], + doc={"arr0": [DOUBLE_NEGATIVE_ZERO], "arr1": [0]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[DOUBLE_NEGATIVE_ZERO, 0]], msg="Should preserve negative zero", ), - ZipTest( + ExpressionTestCase( id="decimal128_nan", - inputs=[[DECIMAL128_NAN], [Decimal128("1")]], + doc={"arr0": [DECIMAL128_NAN], "arr1": [Decimal128("1")]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[DECIMAL128_NAN, Decimal128("1")]], msg="Should preserve Decimal128 NaN", ), ] # Defaults with BSON types -BSON_DEFAULTS_TESTS: list[ZipTest] = [ - ZipTest( +BSON_DEFAULTS_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="default_int64", - inputs=[[1, 2, 3], [Int64(10)]], - use_longest_length=True, - defaults=[0, Int64(0)], + doc={"arr0": [1, 2, 3], "arr1": [Int64(10)]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, Int64(0)], + } + }, expected=[[1, Int64(10)], [2, Int64(0)], [3, Int64(0)]], msg="Should use Int64 default value", ), - ZipTest( + ExpressionTestCase( id="default_decimal128", - inputs=[[1, 2, 3], [Decimal128("1.5")]], - use_longest_length=True, - defaults=[0, Decimal128("0")], + doc={"arr0": [1, 2, 3], "arr1": [Decimal128("1.5")]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, Decimal128("0")], + } + }, expected=[[1, Decimal128("1.5")], [2, Decimal128("0")], [3, Decimal128("0")]], msg="Should use Decimal128 default value", ), - ZipTest( + ExpressionTestCase( id="default_datetime", - inputs=[[1, 2], [datetime(2024, 1, 1, tzinfo=timezone.utc)]], - use_longest_length=True, - defaults=[0, datetime(1970, 1, 1, tzinfo=timezone.utc)], + doc={"arr0": [1, 2], "arr1": [datetime(2024, 1, 1, tzinfo=timezone.utc)]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, datetime(1970, 1, 1, tzinfo=timezone.utc)], + } + }, expected=[ [1, datetime(2024, 1, 1, tzinfo=timezone.utc)], [2, datetime(1970, 1, 1, tzinfo=timezone.utc)], ], msg="Should use datetime default value", ), - ZipTest( + ExpressionTestCase( id="default_objectid", - inputs=[[1, 2], [ObjectId("000000000000000000000001")]], - use_longest_length=True, - defaults=[0, ObjectId("000000000000000000000000")], + doc={"arr0": [1, 2], "arr1": [ObjectId("000000000000000000000001")]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, ObjectId("000000000000000000000000")], + } + }, expected=[ [1, ObjectId("000000000000000000000001")], [2, ObjectId("000000000000000000000000")], ], msg="Should use ObjectId default value", ), - ZipTest( + ExpressionTestCase( id="default_timestamp", - inputs=[[1, 2], [Timestamp(1, 0)]], - use_longest_length=True, - defaults=[0, Timestamp(0, 0)], + doc={"arr0": [1, 2], "arr1": [Timestamp(1, 0)]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, Timestamp(0, 0)], + } + }, expected=[[1, Timestamp(1, 0)], [2, Timestamp(0, 0)]], msg="Should use Timestamp default value", ), - ZipTest( + ExpressionTestCase( id="default_regex", - inputs=[[1, 2], [Regex("^a", "i")]], - use_longest_length=True, - defaults=[0, Regex(".*", "")], + doc={"arr0": [1, 2], "arr1": [Regex("^a", "i")]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, Regex(".*", "")], + } + }, expected=[[1, Regex("^a", "i")], [2, Regex(".*", "")]], msg="Should use Regex default value", ), - ZipTest( + ExpressionTestCase( id="default_minkey_maxkey", - inputs=[[1, 2], [MinKey()]], - use_longest_length=True, - defaults=[0, MaxKey()], + doc={"arr0": [1, 2], "arr1": [MinKey()]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, MaxKey()], + } + }, expected=[[1, MinKey()], [2, MaxKey()]], msg="Should use MaxKey default value", ), - ZipTest( + ExpressionTestCase( id="default_binary", - inputs=[[1, 2], [Binary(b"\x01", 0)]], - use_longest_length=True, - defaults=[0, Binary(b"\x00", 0)], + doc={"arr0": [1, 2], "arr1": [Binary(b"\x01", 0)]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, Binary(b"\x00", 0)], + } + }, expected=[[1, b"\x01"], [2, b"\x00"]], msg="Should use Binary default value", ), ] # Defaults with special numeric values -SPECIAL_NUMERIC_DEFAULTS_TESTS: list[ZipTest] = [ - ZipTest( +SPECIAL_NUMERIC_DEFAULTS_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="default_infinity", - inputs=[[1, 2], [FLOAT_INFINITY]], - use_longest_length=True, - defaults=[0, FLOAT_INFINITY], + doc={"arr0": [1, 2], "arr1": [FLOAT_INFINITY]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, FLOAT_INFINITY], + } + }, expected=[[1, FLOAT_INFINITY], [2, FLOAT_INFINITY]], msg="Should use infinity as default", ), - ZipTest( + ExpressionTestCase( id="default_negative_infinity", - inputs=[[1, 2], [FLOAT_NEGATIVE_INFINITY]], - use_longest_length=True, - defaults=[0, FLOAT_NEGATIVE_INFINITY], + doc={"arr0": [1, 2], "arr1": [FLOAT_NEGATIVE_INFINITY]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, FLOAT_NEGATIVE_INFINITY], + } + }, expected=[[1, FLOAT_NEGATIVE_INFINITY], [2, FLOAT_NEGATIVE_INFINITY]], msg="Should use negative infinity as default", ), - ZipTest( + ExpressionTestCase( id="default_negative_zero", - inputs=[[1, 2], [DOUBLE_NEGATIVE_ZERO]], - use_longest_length=True, - defaults=[0, DOUBLE_NEGATIVE_ZERO], + doc={"arr0": [1, 2], "arr1": [DOUBLE_NEGATIVE_ZERO]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, DOUBLE_NEGATIVE_ZERO], + } + }, expected=[[1, DOUBLE_NEGATIVE_ZERO], [2, DOUBLE_NEGATIVE_ZERO]], msg="Should use negative zero as default", ), - ZipTest( + ExpressionTestCase( id="default_int32_boundaries", - inputs=[[1, 2], [INT32_MIN]], - use_longest_length=True, - defaults=[0, INT32_MAX], + doc={"arr0": [1, 2], "arr1": [INT32_MIN]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, INT32_MAX], + } + }, expected=[[1, INT32_MIN], [2, INT32_MAX]], msg="Should use INT32_MAX as default", ), - ZipTest( + ExpressionTestCase( id="default_int64_boundaries", - inputs=[[1, 2], [INT64_MIN]], - use_longest_length=True, - defaults=[0, INT64_MAX], + doc={"arr0": [1, 2], "arr1": [INT64_MIN]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, INT64_MAX], + } + }, expected=[[1, INT64_MIN], [2, INT64_MAX]], msg="Should use INT64_MAX as default", ), - ZipTest( + ExpressionTestCase( id="default_decimal128_infinity", - inputs=[[1, 2], [DECIMAL128_INFINITY]], - use_longest_length=True, - defaults=[0, DECIMAL128_NEGATIVE_INFINITY], + doc={"arr0": [1, 2], "arr1": [DECIMAL128_INFINITY]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, DECIMAL128_NEGATIVE_INFINITY], + } + }, expected=[[1, DECIMAL128_INFINITY], [2, DECIMAL128_NEGATIVE_INFINITY]], msg="Should use Decimal128 negative infinity as default", ), - ZipTest( + ExpressionTestCase( id="default_decimal128_nan", - inputs=[[1, 2], [DECIMAL128_NAN]], - use_longest_length=True, - defaults=[0, DECIMAL128_NAN], + doc={"arr0": [1, 2], "arr1": [DECIMAL128_NAN]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, DECIMAL128_NAN], + } + }, expected=[[1, DECIMAL128_NAN], [2, DECIMAL128_NAN]], msg="Should use Decimal128 NaN as default", ), @@ -300,35 +390,10 @@ @pytest.mark.parametrize("test", pytest_params(ALL_BSON_TESTS)) def test_zip_bson_insert(collection, test): """Test $zip BSON types with values from inserted documents.""" - expr = {"inputs": [f"$arr{i}" for i in range(len(test.inputs))]} - if test.use_longest_length is not None: - expr["useLongestLength"] = test.use_longest_length - if test.defaults is not None: - expr["defaults"] = test.defaults - doc = {f"arr{i}": arr for i, arr in enumerate(test.inputs)} - result = execute_expression_with_insert(collection, {"$zip": expr}, doc) - assert_expression_result( - result, expected=test.expected, error_code=test.error_code, msg=test.msg - ) - - -TEST_SUBSET_FOR_LITERAL = [ - BSON_TYPE_TESTS[0], # int64_values - BSON_TYPE_TESTS[4], # binary_values - MIXED_BSON_TESTS[0], # mixed_bson_types - SPECIAL_NUMERIC_TESTS[0], # infinity_values -] - - -@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) -def test_zip_bson_literal(collection, test): - """Test $zip BSON types with literal values.""" - expr = {"inputs": [{"$literal": a} for a in test.inputs]} - if test.use_longest_length is not None: - expr["useLongestLength"] = test.use_longest_length - if test.defaults is not None: - expr["defaults"] = test.defaults - result = execute_expression(collection, {"$zip": expr}) + 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/zip/test_zip_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py index 30a1c1891..8701e4743 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py @@ -9,8 +9,8 @@ import pytest from bson import Decimal128, Int64 -from documentdb_tests.compatibility.tests.core.operator.expressions.array.zip.utils.zip_common import ( # noqa: E501 - ZipTest, +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 ( # noqa: E501 assert_expression_result, @@ -21,34 +21,39 @@ # Success: basic zipping — equal length arrays # Property [Basic Transform]: $map applies an expression to each element. -BASIC_TESTS: list[ZipTest] = [ - ZipTest( +BASIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="two_int_arrays", - inputs=[[1, 2, 3], [10, 20, 30]], + doc={"arr0": [1, 2, 3], "arr1": [10, 20, 30]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 10], [2, 20], [3, 30]], msg="Should zip two int arrays", ), - ZipTest( + ExpressionTestCase( id="two_string_arrays", - inputs=[["a", "b"], ["c", "d"]], + doc={"arr0": ["a", "b"], "arr1": ["c", "d"]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[["a", "c"], ["b", "d"]], msg="Should zip two string arrays", ), - ZipTest( + ExpressionTestCase( id="three_arrays", - inputs=[[1, 2], [10, 20], [100, 200]], + doc={"arr0": [1, 2], "arr1": [10, 20], "arr2": [100, 200]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, expected=[[1, 10, 100], [2, 20, 200]], msg="Should zip three arrays", ), - ZipTest( + ExpressionTestCase( id="mixed_type_elements", - inputs=[[1, "two"], [True, None]], + doc={"arr0": [1, "two"], "arr1": [True, None]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, True], ["two", None]], msg="Should zip arrays with mixed types", ), - ZipTest( + ExpressionTestCase( id="numeric_cross_types", - inputs=[[1, Int64(2)], [3.0, Decimal128("4")]], + doc={"arr0": [1, Int64(2)], "arr1": [3.0, Decimal128("4")]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 3.0], [Int64(2), Decimal128("4")]], msg="Should zip mixed numeric type arrays", ), @@ -56,54 +61,57 @@ # Success: unequal length — truncate to shortest (default) # Property [Unequal Length]: $zip truncates to the shortest array. -UNEQUAL_LENGTH_TESTS: list[ZipTest] = [ - ZipTest( +UNEQUAL_LENGTH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="first_shorter", - inputs=[[1, 2], [10, 20, 30]], + doc={"arr0": [1, 2], "arr1": [10, 20, 30]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 10], [2, 20]], msg="Should truncate to shorter first array", ), - ZipTest( + ExpressionTestCase( id="second_shorter", - inputs=[[1, 2, 3], [10, 20]], + doc={"arr0": [1, 2, 3], "arr1": [10, 20]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 10], [2, 20]], msg="Should truncate to shorter second array", ), - ZipTest( + ExpressionTestCase( id="one_empty", - inputs=[[], [1, 2, 3]], + doc={"arr0": [], "arr1": [1, 2, 3]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[], msg="Empty array should produce empty result", ), ] # Success: useLongestLength -USE_LONGEST_TESTS: list[ZipTest] = [ - ZipTest( +USE_LONGEST_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="longest_pads_null", - inputs=[[1, 2, 3], [10, 20]], - use_longest_length=True, + doc={"arr0": [1, 2, 3], "arr1": [10, 20]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, expected=[[1, 10], [2, 20], [3, None]], msg="Should pad shorter array with null", ), - ZipTest( + ExpressionTestCase( id="longest_both_short", - inputs=[[1], [10, 20], [100, 200, 300]], - use_longest_length=True, + doc={"arr0": [1], "arr1": [10, 20], "arr2": [100, 200, 300]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"], "useLongestLength": True}}, expected=[[1, 10, 100], [None, 20, 200], [None, None, 300]], msg="Should pad multiple shorter arrays with null", ), - ZipTest( + ExpressionTestCase( id="longest_equal_length", - inputs=[[1, 2], [10, 20]], - use_longest_length=True, + doc={"arr0": [1, 2], "arr1": [10, 20]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, expected=[[1, 10], [2, 20]], msg="Equal length with useLongestLength should behave same", ), - ZipTest( + ExpressionTestCase( id="longest_false_truncates", - inputs=[[1, 2, 3], [10, 20]], - use_longest_length=False, + doc={"arr0": [1, 2, 3], "arr1": [10, 20]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": False}}, expected=[[1, 10], [2, 20]], msg="useLongestLength false should truncate", ), @@ -111,136 +119,172 @@ # Success: defaults # Property [Defaults]: $zip pads shorter arrays with default values. -DEFAULTS_TESTS: list[ZipTest] = [ - ZipTest( +DEFAULTS_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="defaults_fill_shorter", - inputs=[[1, 2, 3], [10, 20]], - use_longest_length=True, - defaults=[0, 0], + doc={"arr0": [1, 2, 3], "arr1": [10, 20]}, + expression={ + "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0, 0]} + }, expected=[[1, 10], [2, 20], [3, 0]], msg="Should fill shorter array with default value", ), - ZipTest( + ExpressionTestCase( id="defaults_multiple_arrays", - inputs=[[1], [10, 20], [100, 200, 300]], - use_longest_length=True, - defaults=[-1, -2, -3], + doc={"arr0": [1], "arr1": [10, 20], "arr2": [100, 200, 300]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1", "$arr2"], + "useLongestLength": True, + "defaults": [-1, -2, -3], + } + }, expected=[[1, 10, 100], [-1, 20, 200], [-1, -2, 300]], msg="Should fill multiple shorter arrays with respective defaults", ), - ZipTest( + ExpressionTestCase( id="defaults_null_value", - inputs=[[1, 2, 3], [10]], - use_longest_length=True, - defaults=[None, None], + doc={"arr0": [1, 2, 3], "arr1": [10]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [None, None], + } + }, expected=[[1, 10], [2, None], [3, None]], msg="Null defaults should work same as no defaults", ), - ZipTest( + ExpressionTestCase( id="defaults_string", - inputs=[[1, 2, 3], ["a"]], - use_longest_length=True, - defaults=[0, "missing"], + doc={"arr0": [1, 2, 3], "arr1": ["a"]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [0, "missing"], + } + }, expected=[[1, "a"], [2, "missing"], [3, "missing"]], msg="Should use string default value", ), - ZipTest( + ExpressionTestCase( id="defaults_not_used_equal_length", - inputs=[[1, 2], [10, 20]], - use_longest_length=True, - defaults=[0, 0], + doc={"arr0": [1, 2], "arr1": [10, 20]}, + expression={ + "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0, 0]} + }, expected=[[1, 10], [2, 20]], msg="Defaults not used when arrays are equal length", ), - ZipTest( + ExpressionTestCase( id="defaults_falsy_empty_string", - inputs=[[1, 2], ["a"]], - use_longest_length=True, - defaults=[0, ""], + doc={"arr0": [1, 2], "arr1": ["a"]}, + expression={ + "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0, ""]} + }, expected=[[1, "a"], [2, ""]], msg="Falsy defaults (empty string) used correctly", ), - ZipTest( + ExpressionTestCase( id="defaults_false", - inputs=[[1, 2], ["a"]], - use_longest_length=True, - defaults=[0, False], + doc={"arr0": [1, 2], "arr1": ["a"]}, + expression={ + "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0, False]} + }, expected=[[1, "a"], [2, False]], msg="False default used correctly", ), - ZipTest( + ExpressionTestCase( id="defaults_complex_types", - inputs=[[1, 2], ["a"]], - use_longest_length=True, - defaults=[{"x": 1}, [1, 2]], + doc={"arr0": [1, 2], "arr1": ["a"]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [{"x": 1}, [1, 2]], + } + }, expected=[[1, "a"], [2, [1, 2]]], msg="Complex type defaults used correctly", ), - ZipTest( + ExpressionTestCase( id="defaults_nested_array", - inputs=[[1, 2], ["a"]], - use_longest_length=True, - defaults=[[1, 2], "fallback"], + doc={"arr0": [1, 2], "arr1": ["a"]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1"], + "useLongestLength": True, + "defaults": [[1, 2], "fallback"], + } + }, expected=[[1, "a"], [2, "fallback"]], msg="Nested array default used as-is", ), ] # Success: empty and single element -DEGENERATE_TESTS: list[ZipTest] = [ - ZipTest( +DEGENERATE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="both_empty", - inputs=[[], []], + doc={"arr0": [], "arr1": []}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[], msg="Should return empty for two empty arrays", ), - ZipTest( + ExpressionTestCase( id="three_empty", - inputs=[[], [], []], + doc={"arr0": [], "arr1": [], "arr2": []}, + expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, expected=[], msg="Three empty arrays return []", ), - ZipTest( + ExpressionTestCase( id="single_empty", - inputs=[[]], + doc={"arr0": []}, + expression={"$zip": {"inputs": ["$arr0"]}}, expected=[], msg="Single empty array returns []", ), - ZipTest( + ExpressionTestCase( id="single_element_each", - inputs=[[1], [10]], + doc={"arr0": [1], "arr1": [10]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 10]], msg="Should zip single-element arrays", ), - ZipTest( + ExpressionTestCase( id="single_input_array", - inputs=[[1, 2, 3]], + doc={"arr0": [1, 2, 3]}, + expression={"$zip": {"inputs": ["$arr0"]}}, expected=[[1], [2], [3]], msg="Single input should wrap each element", ), - ZipTest( + ExpressionTestCase( id="all_single_element_three", - inputs=[[1], ["a"], [True]], + doc={"arr0": [1], "arr1": ["a"], "arr2": [True]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, expected=[[1, "a", True]], msg="All single-element arrays produce one row", ), - ZipTest( + ExpressionTestCase( id="empty_with_longest_false", - inputs=[[], [1, 2, 3]], + doc={"arr0": [], "arr1": [1, 2, 3]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[], msg="Empty array with shortest length returns []", ), - ZipTest( + ExpressionTestCase( id="empty_with_longest_true", - inputs=[[], [1, 2, 3]], - use_longest_length=True, + doc={"arr0": [], "arr1": [1, 2, 3]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, expected=[[None, 1], [None, 2], [None, 3]], msg="Empty array with longest length pads with null", ), - ZipTest( + ExpressionTestCase( id="two_empty_longest_true", - inputs=[[], []], - use_longest_length=True, + doc={"arr0": [], "arr1": []}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, expected=[], msg="Two empty arrays with longest length return []", ), @@ -248,60 +292,68 @@ # Success: nested arrays as elements # Property [Nested Arrays]: $map operates on nested array structures. -NESTED_ARRAY_TESTS: list[ZipTest] = [ - ZipTest( +NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="nested_arrays", - inputs=[[[1, 2], [3, 4]], ["a", "b"]], + doc={"arr0": [[1, 2], [3, 4]], "arr1": ["a", "b"]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[[1, 2], "a"], [[3, 4], "b"]], msg="Should zip nested arrays as elements", ), - ZipTest( + ExpressionTestCase( id="objects_as_elements", - inputs=[[{"x": 1}, {"x": 2}], [{"y": 10}, {"y": 20}]], + doc={"arr0": [{"x": 1}, {"x": 2}], "arr1": [{"y": 10}, {"y": 20}]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[{"x": 1}, {"y": 10}], [{"x": 2}, {"y": 20}]], msg="Objects preserved as elements", ), - ZipTest( + ExpressionTestCase( id="mixed_types_six", - inputs=[[1, "a", None, True, {"k": 1}, [9]], [10, 20, 30, 40, 50, 60]], + doc={"arr0": [1, "a", None, True, {"k": 1}, [9]], "arr1": [10, 20, 30, 40, 50, 60]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 10], ["a", 20], [None, 30], [True, 40], [{"k": 1}, 50], [[9], 60]], msg="Mixed types preserved in transposition", ), ] # Success: null propagation -NULL_TESTS: list[ZipTest] = [ - ZipTest( +NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="null_first_input", - inputs=[None, [1, 2]], + doc={"arr0": None, "arr1": [1, 2]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=None, msg="Should return null when first input is null", ), - ZipTest( + ExpressionTestCase( id="null_second_input", - inputs=[[1, 2], None], + doc={"arr0": [1, 2], "arr1": None}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=None, msg="Should return null when second input is null", ), - ZipTest( + ExpressionTestCase( id="all_null_inputs", - inputs=[None, None], + doc={"arr0": None, "arr1": None}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=None, msg="Should return null when all inputs are null", ), - ZipTest( + ExpressionTestCase( id="null_elements_in_arrays", - inputs=[[1, None], [None, 2]], + doc={"arr0": [1, None], "arr1": [None, 2]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, None], [None, 2]], msg="Should preserve null elements within arrays", ), ] # Success: objects as elements -OBJECT_TESTS: list[ZipTest] = [ - ZipTest( +OBJECT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="arrays_of_objects", - inputs=[[{"a": 1}], [{"b": 2}]], + doc={"arr0": [{"a": 1}], "arr1": [{"b": 2}]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[{"a": 1}, {"b": 2}]], msg="Should zip arrays of objects", ), @@ -309,43 +361,47 @@ # Success: large arrays # Property [Large Arrays]: $map handles large arrays. -LARGE_ARRAY_TESTS: list[ZipTest] = [ - ZipTest( +LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="large_arrays", - inputs=[list(range(1000)), list(range(1000, 2000))], + doc={"arr0": list(range(1000)), "arr1": list(range(1000, 2000))}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[i, i + 1000] for i in range(1000)], msg="Should zip large arrays", ), ] # Success: many input arrays -MANY_INPUTS_TESTS: list[ZipTest] = [ - ZipTest( +MANY_INPUTS_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="ten_inputs", - inputs=[[i] for i in range(10)], + doc={f"arr{i}": [i] for i in range(10)}, + expression={"$zip": {"inputs": [f"$arr{i}" for i in range(10)]}}, expected=[list(range(10))], msg="Ten inputs transpose correctly", ), - ZipTest( + ExpressionTestCase( id="fifty_inputs", - inputs=[[i] for i in range(50)], + doc={f"arr{i}": [i] for i in range(50)}, + expression={"$zip": {"inputs": [f"$arr{i}" for i in range(50)]}}, expected=[list(range(50))], msg="50 single-element inputs produce one 50-element row", ), ] # Success: multiple arrays of different lengths -MULTI_LENGTH_TESTS: list[ZipTest] = [ - ZipTest( +MULTI_LENGTH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="three_arrays_shortest", - inputs=[[1], [10, 20, 30], [100, 200, 300, 400, 500]], + doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, expected=[[1, 10, 100]], msg="Three arrays shortest = 1", ), - ZipTest( + ExpressionTestCase( id="three_arrays_longest_no_defaults", - inputs=[[1], [10, 20, 30], [100, 200, 300, 400, 500]], - use_longest_length=True, + doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"], "useLongestLength": True}}, expected=[ [1, 10, 100], [None, 20, 200], @@ -355,18 +411,25 @@ ], msg="Three arrays longest pads with null", ), - ZipTest( + ExpressionTestCase( id="three_arrays_longest_with_defaults", - inputs=[[1], [10, 20, 30], [100, 200, 300, 400, 500]], - use_longest_length=True, - defaults=[0, "x", False], + doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1", "$arr2"], + "useLongestLength": True, + "defaults": [0, "x", False], + } + }, expected=[[1, 10, 100], [0, 20, 200], [0, 30, 300], [0, "x", 400], [0, "x", 500]], msg="Three arrays longest with defaults", ), - ZipTest( + ExpressionTestCase( id="four_arrays_two_empty", - inputs=[[], [], [1, 2], [3, 4, 5]], - use_longest_length=True, + doc={"arr0": [], "arr1": [], "arr2": [1, 2], "arr3": [3, 4, 5]}, + expression={ + "$zip": {"inputs": ["$arr0", "$arr1", "$arr2", "$arr3"], "useLongestLength": True} + }, expected=[[None, None, 1, 3], [None, None, 2, 4], [None, None, None, 5]], msg="Four arrays two empty with longest", ), @@ -391,37 +454,10 @@ @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) def test_zip_insert(collection, test): """Test $zip with values from inserted documents.""" - expr = {} - expr["inputs"] = [f"$arr{i}" for i in range(len(test.inputs))] - if test.use_longest_length is not None: - expr["useLongestLength"] = test.use_longest_length - if test.defaults is not None: - expr["defaults"] = test.defaults - doc = {f"arr{i}": arr for i, arr in enumerate(test.inputs)} - result = execute_expression_with_insert(collection, {"$zip": expr}, doc) - assert_expression_result( - result, expected=test.expected, error_code=test.error_code, msg=test.msg - ) - - -TEST_SUBSET_FOR_LITERAL = [ - BASIC_TESTS[0], # two_int_arrays - BASIC_TESTS[2], # three_arrays - UNEQUAL_LENGTH_TESTS[0], # first_shorter - DEGENERATE_TESTS[0], # both_empty -] - - -@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) -def test_zip_literal(collection, test): - """Test $zip with literal values.""" - expr = {} - expr["inputs"] = [{"$literal": a} for a in test.inputs] - if test.use_longest_length is not None: - expr["useLongestLength"] = test.use_longest_length - if test.defaults is not None: - expr["defaults"] = test.defaults - result = execute_expression(collection, {"$zip": expr}) + 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/zip/test_zip_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py index f0eae2fa1..4c90212bb 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.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.zip.utils.zip_common import ( # noqa: E501 - ZipTest, +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 ( # noqa: E501 assert_expression_result, @@ -44,339 +44,375 @@ ) # Error: non-array input element — standard BSON types -NOT_ARRAY_ELEMENT_TESTS: list[ZipTest] = [ - ZipTest( +NOT_ARRAY_ELEMENT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="string_input", - inputs=["hello", [1]], + doc={"arr0": "hello", "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject string input element", ), - ZipTest( + ExpressionTestCase( id="int_input", - inputs=[42, [1]], + doc={"arr0": 42, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject int input element", ), - ZipTest( + ExpressionTestCase( id="negative_int_input", - inputs=[-42, [1]], + doc={"arr0": -42, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject negative int input element", ), - ZipTest( + ExpressionTestCase( id="bool_input", - inputs=[True, [1]], + doc={"arr0": True, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject bool input element", ), - ZipTest( + ExpressionTestCase( id="object_input", - inputs=[{"a": 1}, [1]], + doc={"arr0": {"a": 1}, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject object input element", ), - ZipTest( + ExpressionTestCase( id="double_input", - inputs=[3.14, [1]], + doc={"arr0": 3.14, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject double input element", ), - ZipTest( + ExpressionTestCase( id="negative_double_input", - inputs=[-3.14, [1]], + doc={"arr0": -3.14, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject negative double input element", ), - ZipTest( + ExpressionTestCase( id="decimal128_input", - inputs=[Decimal128("1"), [1]], + doc={"arr0": Decimal128("1"), "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject decimal128 input element", ), - ZipTest( + ExpressionTestCase( id="int64_input", - inputs=[Int64(1), [1]], + doc={"arr0": Int64(1), "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject int64 input element", ), - ZipTest( + ExpressionTestCase( id="objectid_input", - inputs=[ObjectId(), [1]], + doc={"arr0": ObjectId(), "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject objectid input element", ), - ZipTest( + ExpressionTestCase( id="datetime_input", - inputs=[datetime(2024, 1, 1, tzinfo=timezone.utc), [1]], + doc={"arr0": datetime(2024, 1, 1, tzinfo=timezone.utc), "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject datetime input element", ), - ZipTest( + ExpressionTestCase( id="binary_input", - inputs=[Binary(b"x", 0), [1]], + doc={"arr0": Binary(b"x", 0), "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject binary input element", ), - ZipTest( + ExpressionTestCase( id="regex_input", - inputs=[Regex("x"), [1]], + doc={"arr0": Regex("x"), "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject regex input element", ), - ZipTest( + ExpressionTestCase( id="maxkey_input", - inputs=[MaxKey(), [1]], + doc={"arr0": MaxKey(), "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject maxkey input element", ), - ZipTest( + ExpressionTestCase( id="minkey_input", - inputs=[MinKey(), [1]], + doc={"arr0": MinKey(), "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject minkey input element", ), - ZipTest( + ExpressionTestCase( id="timestamp_input", - inputs=[Timestamp(0, 0), [1]], + doc={"arr0": Timestamp(0, 0), "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject timestamp input element", ), - ZipTest( + ExpressionTestCase( id="non_array_second_position", - inputs=[[1], 42], + doc={"arr0": [1], "arr1": 42}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject non-array in second position", ), - ZipTest( + ExpressionTestCase( id="non_array_middle_position", - inputs=[[1], "bad", [2]], + doc={"arr0": [1], "arr1": "bad", "arr2": [2]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject non-array in middle position", ), ] # Error: special float/Decimal128 values as input element -SPECIAL_NUMERIC_ERROR_TESTS: list[ZipTest] = [ - ZipTest( +SPECIAL_NUMERIC_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="nan_input", - inputs=[FLOAT_NAN, [1]], + doc={"arr0": FLOAT_NAN, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject NaN input element", ), - ZipTest( + ExpressionTestCase( id="inf_input", - inputs=[FLOAT_INFINITY, [1]], + doc={"arr0": FLOAT_INFINITY, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject Infinity input element", ), - ZipTest( + ExpressionTestCase( id="neg_inf_input", - inputs=[FLOAT_NEGATIVE_INFINITY, [1]], + doc={"arr0": FLOAT_NEGATIVE_INFINITY, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject -Infinity input element", ), - ZipTest( + ExpressionTestCase( id="neg_zero_input", - inputs=[DOUBLE_NEGATIVE_ZERO, [1]], + doc={"arr0": DOUBLE_NEGATIVE_ZERO, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject negative zero input element", ), - ZipTest( + ExpressionTestCase( id="decimal128_nan_input", - inputs=[DECIMAL128_NAN, [1]], + doc={"arr0": DECIMAL128_NAN, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject Decimal128 NaN input element", ), - ZipTest( + ExpressionTestCase( id="decimal128_inf_input", - inputs=[DECIMAL128_INFINITY, [1]], + doc={"arr0": DECIMAL128_INFINITY, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject Decimal128 Infinity input element", ), - ZipTest( + ExpressionTestCase( id="decimal128_neg_inf_input", - inputs=[DECIMAL128_NEGATIVE_INFINITY, [1]], + doc={"arr0": DECIMAL128_NEGATIVE_INFINITY, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject Decimal128 -Infinity input element", ), - ZipTest( + ExpressionTestCase( id="decimal128_neg_zero_input", - inputs=[DECIMAL128_NEGATIVE_ZERO, [1]], + doc={"arr0": DECIMAL128_NEGATIVE_ZERO, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject Decimal128 -0 input element", ), ] # Error: numeric boundary values as input element -BOUNDARY_ERROR_TESTS: list[ZipTest] = [ - ZipTest( +BOUNDARY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="int32_max_input", - inputs=[INT32_MAX, [1]], + doc={"arr0": INT32_MAX, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject INT32_MAX input element", ), - ZipTest( + ExpressionTestCase( id="int32_min_input", - inputs=[INT32_MIN, [1]], + doc={"arr0": INT32_MIN, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject INT32_MIN input element", ), - ZipTest( + ExpressionTestCase( id="int64_max_input", - inputs=[INT64_MAX, [1]], + doc={"arr0": INT64_MAX, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject INT64_MAX input element", ), - ZipTest( + ExpressionTestCase( id="int64_min_input", - inputs=[INT64_MIN, [1]], + doc={"arr0": INT64_MIN, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject INT64_MIN input element", ), - ZipTest( + ExpressionTestCase( id="decimal128_max_input", - inputs=[DECIMAL128_MAX, [1]], + doc={"arr0": DECIMAL128_MAX, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject DECIMAL128_MAX input element", ), - ZipTest( + ExpressionTestCase( id="decimal128_min_input", - inputs=[DECIMAL128_MIN, [1]], + doc={"arr0": DECIMAL128_MIN, "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject DECIMAL128_MIN input element", ), ] # Error: string edge cases as input element -STRING_EDGE_ERROR_TESTS: list[ZipTest] = [ - ZipTest( +STRING_EDGE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="comma_separated_string_input", - inputs=["1, 2, 3", [1]], + doc={"arr0": "1, 2, 3", "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject comma-separated string", ), - ZipTest( + ExpressionTestCase( id="json_like_string_input", - inputs=["[1, 2, 3]", [1]], + doc={"arr0": "[1, 2, 3]", "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="Should reject JSON-like string", ), ] # Error: invalid useLongestLength -USE_LONGEST_ERROR_TESTS: list[ZipTest] = [ - ZipTest( +USE_LONGEST_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="use_longest_string", - inputs=[[1], [2]], - use_longest_length="true", + doc={"arr0": [1], "arr1": [2]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": "true"}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, msg="Should reject string useLongestLength", ), - ZipTest( + ExpressionTestCase( id="use_longest_int", - inputs=[[1], [2]], - use_longest_length=1, + doc={"arr0": [1], "arr1": [2]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": 1}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, msg="Should reject int useLongestLength", ), - ZipTest( + ExpressionTestCase( id="use_longest_int_0", - inputs=[[1, 2]], - use_longest_length=0, + doc={"arr0": [1, 2]}, + expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": 0}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, msg="Int 0 should error (not bool)", ), - ZipTest( + ExpressionTestCase( id="use_longest_empty_string", - inputs=[[1, 2]], - use_longest_length="", + doc={"arr0": [1, 2]}, + expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": ""}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, msg="Empty string should error (not bool)", ), - ZipTest( + ExpressionTestCase( id="use_longest_array", - inputs=[[1, 2]], - use_longest_length=[], + doc={"arr0": [1, 2]}, + expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": []}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, msg="Empty array should error (not bool)", ), - ZipTest( + ExpressionTestCase( id="use_longest_object", - inputs=[[1, 2]], - use_longest_length={"a": 1}, + doc={"arr0": [1, 2]}, + expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": {"a": 1}}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, msg="Object should error (not bool)", ), - ZipTest( + ExpressionTestCase( id="use_longest_nan", - inputs=[[1, 2]], - use_longest_length=float("nan"), + doc={"arr0": [1, 2]}, + expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": float("nan")}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, msg="NaN should error (not bool)", ), - ZipTest( + ExpressionTestCase( id="use_longest_infinity", - inputs=[[1, 2]], - use_longest_length=float("inf"), + doc={"arr0": [1, 2]}, + expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": float("inf")}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, msg="Infinity should error (not bool)", ), ] # Error: invalid defaults -DEFAULTS_ERROR_TESTS: list[ZipTest] = [ - ZipTest( +DEFAULTS_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( id="defaults_without_use_longest", - inputs=[[1, 2], [3]], - defaults=[0, 0], + doc={"arr0": [1, 2], "arr1": [3]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"], "defaults": [0, 0]}}, error_code=ZIP_DEFAULTS_WITHOUT_LONGEST_ERROR, msg="Should reject defaults without useLongestLength", ), - ZipTest( + ExpressionTestCase( id="defaults_without_longest_false", - inputs=[[1], [2]], - use_longest_length=False, - defaults=[0, 0], + doc={"arr0": [1], "arr1": [2]}, + expression={ + "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": False, "defaults": [0, 0]} + }, error_code=ZIP_DEFAULTS_WITHOUT_LONGEST_ERROR, msg="defaults with useLongestLength false should error", ), - ZipTest( + ExpressionTestCase( id="defaults_length_mismatch", - inputs=[[1, 2], [3]], - use_longest_length=True, - defaults=[0], + doc={"arr0": [1, 2], "arr1": [3]}, + expression={ + "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0]} + }, error_code=ZIP_DEFAULTS_LENGTH_MISMATCH_ERROR, msg="Should reject defaults with wrong length", ), - ZipTest( + ExpressionTestCase( id="defaults_too_many", - inputs=[[1], [2]], - use_longest_length=True, - defaults=[0, 0, 0], + doc={"arr0": [1], "arr1": [2]}, + expression={ + "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0, 0, 0]} + }, error_code=ZIP_DEFAULTS_LENGTH_MISMATCH_ERROR, msg="Should reject defaults longer than inputs", ), - ZipTest( + ExpressionTestCase( id="defaults_not_array", - inputs=[[1], [2]], - use_longest_length=True, - defaults="bad", + doc={"arr0": [1], "arr1": [2]}, + expression={ + "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": "bad"} + }, error_code=ZIP_DEFAULTS_NOT_ARRAY_ERROR, msg="Should reject non-array defaults", ), - ZipTest( + ExpressionTestCase( id="defaults_not_array_object", - inputs=[[1]], - use_longest_length=True, - defaults={"a": 1}, + doc={"arr0": [1]}, + expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": True, "defaults": {"a": 1}}}, error_code=ZIP_DEFAULTS_NOT_ARRAY_ERROR, msg="defaults as object should error", ), - ZipTest( + ExpressionTestCase( id="defaults_not_array_int", - inputs=[[1]], - use_longest_length=True, - defaults=1, + doc={"arr0": [1]}, + expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": True, "defaults": 1}}, error_code=ZIP_DEFAULTS_NOT_ARRAY_ERROR, msg="defaults as int should error", ), @@ -396,35 +432,10 @@ @pytest.mark.parametrize("test", pytest_params(ALL_INPUT_ELEMENT_TESTS)) def test_zip_not_array_insert(collection, test): """Test $zip error with non-array input element from inserted documents.""" - expr = {"inputs": [f"$arr{i}" for i in range(len(test.inputs))]} - if test.use_longest_length is not None: - expr["useLongestLength"] = test.use_longest_length - if test.defaults is not None: - expr["defaults"] = test.defaults - doc = {f"arr{i}": arr for i, arr in enumerate(test.inputs)} - result = execute_expression_with_insert(collection, {"$zip": expr}, doc) - assert_expression_result( - result, expected=test.expected, error_code=test.error_code, msg=test.msg - ) - - -TEST_SUBSET_FOR_LITERAL = [ - NOT_ARRAY_ELEMENT_TESTS[0], # string_input - NOT_ARRAY_ELEMENT_TESTS[-2], # non_array_second_position - SPECIAL_NUMERIC_ERROR_TESTS[0], # nan_input - BOUNDARY_ERROR_TESTS[0], # int32_max_input -] - - -@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) -def test_zip_not_array_literal(collection, test): - """Test $zip error with non-array literal input element.""" - expr = {"inputs": [{"$literal": a} if isinstance(a, list) else a for a in test.inputs]} - if test.use_longest_length is not None: - expr["useLongestLength"] = test.use_longest_length - if test.defaults is not None: - expr["defaults"] = test.defaults - result = execute_expression(collection, {"$zip": expr}) + 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/zip/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/zip_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/zip_common.py deleted file mode 100644 index 40c5f6bf7..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/utils/zip_common.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Shared test infrastructure for $zip expression tests. -""" - -from dataclasses import dataclass -from typing import Any - -from documentdb_tests.framework.test_case import BaseTestCase - - -@dataclass(frozen=True) -class ZipTest(BaseTestCase): - """Test case for $zip operator.""" - - inputs: Any = None - use_longest_length: Any = None - defaults: Any = None From 869c2c41e98a49de8c7a7e13ad4b3adc1d7e41df Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 8 Jul 2026 11:26:44 -0700 Subject: [PATCH 06/17] convert ARITY_ERROR_TESTS to use ExpressionTestCase Signed-off-by: Alina (Xi) Li --- .../array/range/test_range_errors.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py index 18ca191c7..07ebd88a5 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py @@ -593,6 +593,28 @@ ] # Aggregate and test +# Error: wrong arity +ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="zero_args", + expression={"$range": []}, + error_code=EXPRESSION_ARITY_ERROR, + msg="Should error with zero arguments", + ), + ExpressionTestCase( + id="one_arg", + expression={"$range": [1]}, + error_code=EXPRESSION_ARITY_ERROR, + msg="Should error with one argument", + ), + ExpressionTestCase( + id="four_args", + expression={"$range": [1, 2, 3, 4]}, + error_code=EXPRESSION_ARITY_ERROR, + msg="Should error with four arguments", + ), +] + ALL_TESTS = ( NON_NUMERIC_START_TESTS + NON_NUMERIC_END_TESTS @@ -603,6 +625,7 @@ + SPECIAL_NUMERIC_TESTS + STEP_ZERO_TESTS + OUT_OF_INT32_TESTS + + ARITY_ERROR_TESTS ) @@ -616,18 +639,3 @@ def test_range_error_insert(collection, test): assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) - - -# Error: wrong arity -ARITY_ERROR_TESTS = [ - pytest.param({"$range": []}, id="zero_args"), - pytest.param({"$range": [1]}, id="one_arg"), - pytest.param({"$range": [1, 2, 3, 4]}, id="four_args"), -] - - -@pytest.mark.parametrize("expr", ARITY_ERROR_TESTS) -def test_range_arity_error(collection, expr): - """Test $range errors with wrong number of arguments.""" - result = execute_expression(collection, expr) - assert_expression_result(result, error_code=EXPRESSION_ARITY_ERROR) From 497a6bfed3b79e98ccece0c7dd4eb26fb75b0815 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 8 Jul 2026 12:01:32 -0700 Subject: [PATCH 07/17] separate tests to different files Signed-off-by: Alina (Xi) Li --- .../array/range/test_range_core_behavior.py | 211 +------------ .../range/test_range_numeric_acceptance.py | 224 ++++++++++++++ .../array/range/test_range_type_errors.py | 292 ++++++++++++++++++ ...e_errors.py => test_range_value_errors.py} | 283 +---------------- .../array/zip/test_zip_core_behavior.py | 238 +------------- .../array/zip/test_zip_degenerate_cases.py | 252 +++++++++++++++ 6 files changed, 783 insertions(+), 717 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_type_errors.py rename documentdb_tests/compatibility/tests/core/operator/expressions/array/range/{test_range_errors.py => test_range_value_errors.py} (56%) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py index 2cbb17fc7..a14f37fc7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py @@ -2,18 +2,16 @@ Core behavior tests for $range expression. Tests generating integer sequences with various start, end, step values, -negative ranges, empty results, and large ranges. +negative steps (descending), and empty results. """ 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 ( # noqa: E501 assert_expression_result, - execute_expression, execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params @@ -217,216 +215,13 @@ ), ] -# Success: numeric type acceptance (int64, whole doubles, whole decimal128) -NUMERIC_TYPE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="int64_args", - doc={"start": Int64(0), "end": Int64(3)}, - expression={"$range": ["$start", "$end"]}, - expected=[0, 1, 2], - msg="Should accept Int64 arguments", - ), - ExpressionTestCase( - id="whole_double_args", - doc={"start": 0.0, "end": 3.0}, - expression={"$range": ["$start", "$end"]}, - expected=[0, 1, 2], - msg="Should accept whole-number double arguments", - ), - ExpressionTestCase( - id="whole_decimal128_args", - doc={"start": Decimal128("0"), "end": Decimal128("3")}, - expression={"$range": ["$start", "$end"]}, - expected=[0, 1, 2], - msg="Should accept whole-number Decimal128 arguments", - ), - ExpressionTestCase( - id="int64_step", - doc={"start": 0, "end": 6, "step": Int64(2)}, - expression={"$range": ["$start", "$end", "$step"]}, - expected=[0, 2, 4], - msg="Should accept Int64 step", - ), - ExpressionTestCase( - id="whole_double_step", - doc={"start": 0, "end": 6, "step": 2.0}, - expression={"$range": ["$start", "$end", "$step"]}, - expected=[0, 2, 4], - msg="Should accept whole-number double step", - ), - ExpressionTestCase( - id="whole_decimal128_step", - doc={"start": 0, "end": 6, "step": Decimal128("2")}, - expression={"$range": ["$start", "$end", "$step"]}, - expected=[0, 2, 4], - msg="Should accept whole-number Decimal128 step", - ), - ExpressionTestCase( - id="decimal128_trailing_zero_start", - doc={"start": Decimal128("0.0"), "end": 3}, - expression={"$range": ["$start", "$end"]}, - expected=[0, 1, 2], - msg="Should accept Decimal128 0.0 as start", - ), - ExpressionTestCase( - id="decimal128_trailing_zero_end", - doc={"start": 0, "end": Decimal128("3.0")}, - expression={"$range": ["$start", "$end"]}, - expected=[0, 1, 2], - msg="Should accept Decimal128 3.0 as end", - ), - ExpressionTestCase( - id="decimal128_trailing_zero_both", - doc={"start": Decimal128("0.0"), "end": Decimal128("3.0")}, - expression={"$range": ["$start", "$end"]}, - expected=[0, 1, 2], - msg="Should accept Decimal128 0.0 start and 3.0 end", - ), -] - -# Success: single element result -# Property [Single Element]: $range produces single-element arrays. -SINGLE_ELEMENT_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="single_element", - doc={"start": 0, "end": 1}, - expression={"$range": ["$start", "$end"]}, - expected=[0], - msg="Should return single element", - ), - ExpressionTestCase( - id="desc_single", - doc={"start": 1, "end": 0, "step": -1}, - expression={"$range": ["$start", "$end", "$step"]}, - expected=[1], - msg="Descending single element", - ), - ExpressionTestCase( - id="step_eq_range", - doc={"start": 0, "end": 5, "step": 5}, - expression={"$range": ["$start", "$end", "$step"]}, - expected=[0], - msg="Step equal to range should produce single element", - ), - ExpressionTestCase( - id="step_gt_range", - doc={"start": 0, "end": 5, "step": 10}, - expression={"$range": ["$start", "$end", "$step"]}, - expected=[0], - msg="Step greater than range should produce single element", - ), -] - -# Success: negative number ranges -NEGATIVE_RANGE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="neg_to_zero", - doc={"start": -5, "end": 0}, - expression={"$range": ["$start", "$end"]}, - expected=[-5, -4, -3, -2, -1], - msg="Negative start to zero ascending", - ), - ExpressionTestCase( - id="zero_to_neg", - doc={"start": 0, "end": -5, "step": -1}, - expression={"$range": ["$start", "$end", "$step"]}, - expected=[0, -1, -2, -3, -4], - msg="Zero to negative descending", - ), - ExpressionTestCase( - id="cross_zero_asc", - doc={"start": -3, "end": 3}, - expression={"$range": ["$start", "$end"]}, - expected=[-3, -2, -1, 0, 1, 2], - msg="Crossing zero ascending", - ), - ExpressionTestCase( - id="cross_zero_desc", - doc={"start": 3, "end": -3, "step": -1}, - expression={"$range": ["$start", "$end", "$step"]}, - expected=[3, 2, 1, 0, -1, -2], - msg="Crossing zero descending", - ), - ExpressionTestCase( - id="neg_desc_step2", - doc={"start": -10, "end": -20, "step": -2}, - expression={"$range": ["$start", "$end", "$step"]}, - expected=[-10, -12, -14, -16, -18], - msg="Negative descending with step -2", - ), - ExpressionTestCase( - id="neg_asc_step2", - doc={"start": -10, "end": -1, "step": 2}, - expression={"$range": ["$start", "$end", "$step"]}, - expected=[-10, -8, -6, -4, -2], - msg="Negative ascending with step 2", - ), -] - -# Success: large range -LARGE_RANGE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="large_range", - doc={"start": 0, "end": 1000}, - expression={"$range": ["$start", "$end"]}, - expected=list(range(1000)), - msg="Should generate large range", - ), -] - -# Success: negative zero start/end -NEG_ZERO_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="neg_zero_double_start", - doc={"start": -0.0, "end": 5}, - expression={"$range": ["$start", "$end"]}, - expected=[0, 1, 2, 3, 4], - msg="Negative zero double start treated as 0", - ), - ExpressionTestCase( - id="neg_zero_decimal_start", - doc={"start": Decimal128("-0"), "end": 5}, - expression={"$range": ["$start", "$end"]}, - expected=[0, 1, 2, 3, 4], - msg="Negative zero Decimal128 start treated as 0", - ), - ExpressionTestCase( - id="neg_zero_double_end", - doc={"start": 0, "end": -0.0}, - expression={"$range": ["$start", "$end"]}, - expected=[], - msg="Negative zero double end treated as 0", - ), - ExpressionTestCase( - id="neg_zero_decimal_end", - doc={"start": 0, "end": Decimal128("-0")}, - expression={"$range": ["$start", "$end"]}, - expected=[], - msg="Negative zero Decimal128 end treated as 0", - ), -] - -# Aggregate and test -ALL_TESTS = ( - BASIC_ASC_TESTS - + STEP_TESTS - + NEGATIVE_STEP_TESTS - + EMPTY_TESTS - + NUMERIC_TYPE_TESTS - + SINGLE_ELEMENT_TESTS - + NEGATIVE_RANGE_TESTS - + LARGE_RANGE_TESTS - + NEG_ZERO_TESTS -) +ALL_TESTS = BASIC_ASC_TESTS + STEP_TESTS + NEGATIVE_STEP_TESTS + EMPTY_TESTS @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) def test_range_insert(collection, test): """Test $range 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) + 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/range/test_range_numeric_acceptance.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py new file mode 100644 index 000000000..220f0e139 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py @@ -0,0 +1,224 @@ +""" +Numeric type acceptance tests for $range expression. + +Tests Int64, whole doubles, whole Decimal128, single-element results, +negative number ranges, large ranges, and negative zero handling. +""" + +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 ( # noqa: E501 + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Success: numeric type acceptance (int64, whole doubles, whole decimal128) +NUMERIC_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="int64_args", + doc={"start": Int64(0), "end": Int64(3)}, + expression={"$range": ["$start", "$end"]}, + expected=[0, 1, 2], + msg="Should accept Int64 arguments", + ), + ExpressionTestCase( + id="whole_double_args", + doc={"start": 0.0, "end": 3.0}, + expression={"$range": ["$start", "$end"]}, + expected=[0, 1, 2], + msg="Should accept whole-number double arguments", + ), + ExpressionTestCase( + id="whole_decimal128_args", + doc={"start": Decimal128("0"), "end": Decimal128("3")}, + expression={"$range": ["$start", "$end"]}, + expected=[0, 1, 2], + msg="Should accept whole-number Decimal128 arguments", + ), + ExpressionTestCase( + id="int64_step", + doc={"start": 0, "end": 6, "step": Int64(2)}, + expression={"$range": ["$start", "$end", "$step"]}, + expected=[0, 2, 4], + msg="Should accept Int64 step", + ), + ExpressionTestCase( + id="whole_double_step", + doc={"start": 0, "end": 6, "step": 2.0}, + expression={"$range": ["$start", "$end", "$step"]}, + expected=[0, 2, 4], + msg="Should accept whole-number double step", + ), + ExpressionTestCase( + id="whole_decimal128_step", + doc={"start": 0, "end": 6, "step": Decimal128("2")}, + expression={"$range": ["$start", "$end", "$step"]}, + expected=[0, 2, 4], + msg="Should accept whole-number Decimal128 step", + ), + ExpressionTestCase( + id="decimal128_trailing_zero_start", + doc={"start": Decimal128("0.0"), "end": 3}, + expression={"$range": ["$start", "$end"]}, + expected=[0, 1, 2], + msg="Should accept Decimal128 0.0 as start", + ), + ExpressionTestCase( + id="decimal128_trailing_zero_end", + doc={"start": 0, "end": Decimal128("3.0")}, + expression={"$range": ["$start", "$end"]}, + expected=[0, 1, 2], + msg="Should accept Decimal128 3.0 as end", + ), + ExpressionTestCase( + id="decimal128_trailing_zero_both", + doc={"start": Decimal128("0.0"), "end": Decimal128("3.0")}, + expression={"$range": ["$start", "$end"]}, + expected=[0, 1, 2], + msg="Should accept Decimal128 0.0 start and 3.0 end", + ), +] + +# Success: single element result +# Property [Single Element]: $range produces single-element arrays. +SINGLE_ELEMENT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="single_element", + doc={"start": 0, "end": 1}, + expression={"$range": ["$start", "$end"]}, + expected=[0], + msg="Should return single element", + ), + ExpressionTestCase( + id="desc_single", + doc={"start": 1, "end": 0, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, + expected=[1], + msg="Descending single element", + ), + ExpressionTestCase( + id="step_eq_range", + doc={"start": 0, "end": 5, "step": 5}, + expression={"$range": ["$start", "$end", "$step"]}, + expected=[0], + msg="Step equal to range should produce single element", + ), + ExpressionTestCase( + id="step_gt_range", + doc={"start": 0, "end": 5, "step": 10}, + expression={"$range": ["$start", "$end", "$step"]}, + expected=[0], + msg="Step greater than range should produce single element", + ), +] + +# Success: negative number ranges +NEGATIVE_RANGE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="neg_to_zero", + doc={"start": -5, "end": 0}, + expression={"$range": ["$start", "$end"]}, + expected=[-5, -4, -3, -2, -1], + msg="Negative start to zero ascending", + ), + ExpressionTestCase( + id="zero_to_neg", + doc={"start": 0, "end": -5, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, + expected=[0, -1, -2, -3, -4], + msg="Zero to negative descending", + ), + ExpressionTestCase( + id="cross_zero_asc", + doc={"start": -3, "end": 3}, + expression={"$range": ["$start", "$end"]}, + expected=[-3, -2, -1, 0, 1, 2], + msg="Crossing zero ascending", + ), + ExpressionTestCase( + id="cross_zero_desc", + doc={"start": 3, "end": -3, "step": -1}, + expression={"$range": ["$start", "$end", "$step"]}, + expected=[3, 2, 1, 0, -1, -2], + msg="Crossing zero descending", + ), + ExpressionTestCase( + id="neg_desc_step2", + doc={"start": -10, "end": -20, "step": -2}, + expression={"$range": ["$start", "$end", "$step"]}, + expected=[-10, -12, -14, -16, -18], + msg="Negative descending with step -2", + ), + ExpressionTestCase( + id="neg_asc_step2", + doc={"start": -10, "end": -1, "step": 2}, + expression={"$range": ["$start", "$end", "$step"]}, + expected=[-10, -8, -6, -4, -2], + msg="Negative ascending with step 2", + ), +] + +# Success: large range +LARGE_RANGE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="large_range", + doc={"start": 0, "end": 1000}, + expression={"$range": ["$start", "$end"]}, + expected=list(range(1000)), + msg="Should generate large range", + ), +] + +# Success: negative zero start/end +NEG_ZERO_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="neg_zero_double_start", + doc={"start": -0.0, "end": 5}, + expression={"$range": ["$start", "$end"]}, + expected=[0, 1, 2, 3, 4], + msg="Negative zero double start treated as 0", + ), + ExpressionTestCase( + id="neg_zero_decimal_start", + doc={"start": Decimal128("-0"), "end": 5}, + expression={"$range": ["$start", "$end"]}, + expected=[0, 1, 2, 3, 4], + msg="Negative zero Decimal128 start treated as 0", + ), + ExpressionTestCase( + id="neg_zero_double_end", + doc={"start": 0, "end": -0.0}, + expression={"$range": ["$start", "$end"]}, + expected=[], + msg="Negative zero double end treated as 0", + ), + ExpressionTestCase( + id="neg_zero_decimal_end", + doc={"start": 0, "end": Decimal128("-0")}, + expression={"$range": ["$start", "$end"]}, + expected=[], + msg="Negative zero Decimal128 end treated as 0", + ), +] + +ALL_TESTS = ( + NUMERIC_TYPE_TESTS + + SINGLE_ELEMENT_TESTS + + NEGATIVE_RANGE_TESTS + + LARGE_RANGE_TESTS + + NEG_ZERO_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_range_numeric_acceptance(collection, test): + """Test $range with various accepted numeric types and value patterns.""" + 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/range/test_range_type_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_type_errors.py new file mode 100644 index 000000000..43db6c022 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_type_errors.py @@ -0,0 +1,292 @@ +""" +Type error tests for $range expression. + +Tests non-numeric types for start, end, and step positions. +""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, 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 ( # noqa: E501 + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + RANGE_END_NOT_NUMERIC_ERROR, + RANGE_START_NOT_INT32_ERROR, + RANGE_STEP_NOT_NUMERIC_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Error: non-numeric start +NON_NUMERIC_START_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="string_start", + doc={"start": "hello", "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject string start", + ), + ExpressionTestCase( + id="bool_start", + doc={"start": True, "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject bool start", + ), + ExpressionTestCase( + id="null_start", + doc={"start": None, "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject null start", + ), + ExpressionTestCase( + id="object_start", + doc={"start": {"a": 1}, "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject object start", + ), + ExpressionTestCase( + id="array_start", + doc={"start": [1], "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject array start", + ), + ExpressionTestCase( + id="objectid_start", + doc={"start": ObjectId(), "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject objectid start", + ), + ExpressionTestCase( + id="datetime_start", + doc={"start": datetime(2024, 1, 1, tzinfo=timezone.utc), "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject datetime start", + ), + ExpressionTestCase( + id="binary_start", + doc={"start": Binary(b"x", 0), "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject binary start", + ), + ExpressionTestCase( + id="regex_start", + doc={"start": Regex("x"), "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject regex start", + ), + ExpressionTestCase( + id="maxkey_start", + doc={"start": MaxKey(), "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject maxkey start", + ), + ExpressionTestCase( + id="minkey_start", + doc={"start": MinKey(), "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject minkey start", + ), + ExpressionTestCase( + id="timestamp_start", + doc={"start": Timestamp(0, 0), "end": 5}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_START_NOT_INT32_ERROR, + msg="Should reject timestamp start", + ), +] + +# Error: non-numeric end +NON_NUMERIC_END_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="string_end", + doc={"start": 0, "end": "hello"}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject string end", + ), + ExpressionTestCase( + id="bool_end", + doc={"start": 0, "end": True}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject bool end", + ), + ExpressionTestCase( + id="null_end", + doc={"start": 0, "end": None}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject null end", + ), + ExpressionTestCase( + id="object_end", + doc={"start": 0, "end": {"a": 1}}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject object end", + ), + ExpressionTestCase( + id="array_end", + doc={"start": 0, "end": [1]}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject array end", + ), + ExpressionTestCase( + id="objectid_end", + doc={"start": 0, "end": ObjectId()}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject objectid end", + ), + ExpressionTestCase( + id="datetime_end", + doc={"start": 0, "end": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject datetime end", + ), + ExpressionTestCase( + id="binary_end", + doc={"start": 0, "end": Binary(b"x", 0)}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject binary end", + ), + ExpressionTestCase( + id="regex_end", + doc={"start": 0, "end": Regex("x")}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject regex end", + ), + ExpressionTestCase( + id="maxkey_end", + doc={"start": 0, "end": MaxKey()}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject maxkey end", + ), + ExpressionTestCase( + id="minkey_end", + doc={"start": 0, "end": MinKey()}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject minkey end", + ), + ExpressionTestCase( + id="timestamp_end", + doc={"start": 0, "end": Timestamp(0, 0)}, + expression={"$range": ["$start", "$end"]}, + error_code=RANGE_END_NOT_NUMERIC_ERROR, + msg="Should reject timestamp end", + ), +] + +# Error: non-numeric step +NON_NUMERIC_STEP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="string_step", + doc={"start": 0, "end": 5, "step": "bad"}, + expression={"$range": ["$start", "$end", "$step"]}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject string step", + ), + ExpressionTestCase( + id="bool_step", + doc={"start": 0, "end": 5, "step": True}, + expression={"$range": ["$start", "$end", "$step"]}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject bool step", + ), + ExpressionTestCase( + id="object_step", + doc={"start": 0, "end": 5, "step": {"a": 1}}, + expression={"$range": ["$start", "$end", "$step"]}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject object step", + ), + ExpressionTestCase( + id="array_step", + doc={"start": 0, "end": 5, "step": [1]}, + expression={"$range": ["$start", "$end", "$step"]}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject array step", + ), + ExpressionTestCase( + id="objectid_step", + doc={"start": 0, "end": 5, "step": ObjectId()}, + expression={"$range": ["$start", "$end", "$step"]}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject objectid step", + ), + ExpressionTestCase( + id="datetime_step", + doc={"start": 0, "end": 5, "step": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + expression={"$range": ["$start", "$end", "$step"]}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject datetime step", + ), + ExpressionTestCase( + id="binary_step", + doc={"start": 0, "end": 5, "step": Binary(b"x", 0)}, + expression={"$range": ["$start", "$end", "$step"]}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject binary step", + ), + ExpressionTestCase( + id="regex_step", + doc={"start": 0, "end": 5, "step": Regex("x")}, + expression={"$range": ["$start", "$end", "$step"]}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject regex step", + ), + ExpressionTestCase( + id="maxkey_step", + doc={"start": 0, "end": 5, "step": MaxKey()}, + expression={"$range": ["$start", "$end", "$step"]}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject maxkey step", + ), + ExpressionTestCase( + id="minkey_step", + doc={"start": 0, "end": 5, "step": MinKey()}, + expression={"$range": ["$start", "$end", "$step"]}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject minkey step", + ), + ExpressionTestCase( + id="timestamp_step", + doc={"start": 0, "end": 5, "step": Timestamp(0, 0)}, + expression={"$range": ["$start", "$end", "$step"]}, + error_code=RANGE_STEP_NOT_NUMERIC_ERROR, + msg="Should reject timestamp step", + ), +] + +ALL_TESTS = NON_NUMERIC_START_TESTS + NON_NUMERIC_END_TESTS + NON_NUMERIC_STEP_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_range_type_error(collection, test): + """Test $range error with non-numeric types for start, end, step.""" + 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/range/test_range_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py similarity index 56% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py index 07ebd88a5..92fd48c4e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py @@ -1,15 +1,12 @@ """ -Error tests for $range expression. - -Tests non-numeric types, non-integral values, step zero, out-of-range -int32 values, and wrong arity. +Value error tests for $range expression. +Tests non-integral values, special numeric values (NaN, Infinity), +step zero, out-of-int32-range values, and wrong arity. """ -from datetime import datetime, timezone - import pytest -from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp +from bson import Decimal128, Int64 from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 ExpressionTestCase, @@ -22,11 +19,8 @@ from documentdb_tests.framework.error_codes import ( EXPRESSION_ARITY_ERROR, RANGE_END_NOT_INT32_ERROR, - RANGE_END_NOT_NUMERIC_ERROR, - RANGE_START_NOT_INT32_ERROR, RANGE_START_NOT_INTEGRAL_ERROR, RANGE_STEP_NOT_INT32_ERROR, - RANGE_STEP_NOT_NUMERIC_ERROR, RANGE_STEP_ZERO_ERROR, ) from documentdb_tests.framework.parametrize import pytest_params @@ -43,263 +37,6 @@ INT64_MIN, ) -# Error: non-numeric start -NON_NUMERIC_START_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="string_start", - doc={"start": "hello", "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject string start", - ), - ExpressionTestCase( - id="bool_start", - doc={"start": True, "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject bool start", - ), - ExpressionTestCase( - id="null_start", - doc={"start": None, "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject null start", - ), - ExpressionTestCase( - id="object_start", - doc={"start": {"a": 1}, "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject object start", - ), - ExpressionTestCase( - id="array_start", - doc={"start": [1], "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject array start", - ), - ExpressionTestCase( - id="objectid_start", - doc={"start": ObjectId(), "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject objectid start", - ), - ExpressionTestCase( - id="datetime_start", - doc={"start": datetime(2024, 1, 1, tzinfo=timezone.utc), "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject datetime start", - ), - ExpressionTestCase( - id="binary_start", - doc={"start": Binary(b"x", 0), "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject binary start", - ), - ExpressionTestCase( - id="regex_start", - doc={"start": Regex("x"), "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject regex start", - ), - ExpressionTestCase( - id="maxkey_start", - doc={"start": MaxKey(), "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject maxkey start", - ), - ExpressionTestCase( - id="minkey_start", - doc={"start": MinKey(), "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject minkey start", - ), - ExpressionTestCase( - id="timestamp_start", - doc={"start": Timestamp(0, 0), "end": 5}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject timestamp start", - ), -] - -# Error: non-numeric end -NON_NUMERIC_END_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="string_end", - doc={"start": 0, "end": "hello"}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject string end", - ), - ExpressionTestCase( - id="bool_end", - doc={"start": 0, "end": True}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject bool end", - ), - ExpressionTestCase( - id="null_end", - doc={"start": 0, "end": None}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject null end", - ), - ExpressionTestCase( - id="object_end", - doc={"start": 0, "end": {"a": 1}}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject object end", - ), - ExpressionTestCase( - id="array_end", - doc={"start": 0, "end": [1]}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject array end", - ), - ExpressionTestCase( - id="objectid_end", - doc={"start": 0, "end": ObjectId()}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject objectid end", - ), - ExpressionTestCase( - id="datetime_end", - doc={"start": 0, "end": datetime(2024, 1, 1, tzinfo=timezone.utc)}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject datetime end", - ), - ExpressionTestCase( - id="binary_end", - doc={"start": 0, "end": Binary(b"x", 0)}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject binary end", - ), - ExpressionTestCase( - id="regex_end", - doc={"start": 0, "end": Regex("x")}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject regex end", - ), - ExpressionTestCase( - id="maxkey_end", - doc={"start": 0, "end": MaxKey()}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject maxkey end", - ), - ExpressionTestCase( - id="minkey_end", - doc={"start": 0, "end": MinKey()}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject minkey end", - ), - ExpressionTestCase( - id="timestamp_end", - doc={"start": 0, "end": Timestamp(0, 0)}, - expression={"$range": ["$start", "$end"]}, - error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject timestamp end", - ), -] - -# Error: non-numeric step -NON_NUMERIC_STEP_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="string_step", - doc={"start": 0, "end": 5, "step": "bad"}, - expression={"$range": ["$start", "$end", "$step"]}, - error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject string step", - ), - ExpressionTestCase( - id="bool_step", - doc={"start": 0, "end": 5, "step": True}, - expression={"$range": ["$start", "$end", "$step"]}, - error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject bool step", - ), - ExpressionTestCase( - id="object_step", - doc={"start": 0, "end": 5, "step": {"a": 1}}, - expression={"$range": ["$start", "$end", "$step"]}, - error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject object step", - ), - ExpressionTestCase( - id="array_step", - doc={"start": 0, "end": 5, "step": [1]}, - expression={"$range": ["$start", "$end", "$step"]}, - error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject array step", - ), - ExpressionTestCase( - id="objectid_step", - doc={"start": 0, "end": 5, "step": ObjectId()}, - expression={"$range": ["$start", "$end", "$step"]}, - error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject objectid step", - ), - ExpressionTestCase( - id="datetime_step", - doc={"start": 0, "end": 5, "step": datetime(2024, 1, 1, tzinfo=timezone.utc)}, - expression={"$range": ["$start", "$end", "$step"]}, - error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject datetime step", - ), - ExpressionTestCase( - id="binary_step", - doc={"start": 0, "end": 5, "step": Binary(b"x", 0)}, - expression={"$range": ["$start", "$end", "$step"]}, - error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject binary step", - ), - ExpressionTestCase( - id="regex_step", - doc={"start": 0, "end": 5, "step": Regex("x")}, - expression={"$range": ["$start", "$end", "$step"]}, - error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject regex step", - ), - ExpressionTestCase( - id="maxkey_step", - doc={"start": 0, "end": 5, "step": MaxKey()}, - expression={"$range": ["$start", "$end", "$step"]}, - error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject maxkey step", - ), - ExpressionTestCase( - id="minkey_step", - doc={"start": 0, "end": 5, "step": MinKey()}, - expression={"$range": ["$start", "$end", "$step"]}, - error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject minkey step", - ), - ExpressionTestCase( - id="timestamp_step", - doc={"start": 0, "end": 5, "step": Timestamp(0, 0)}, - expression={"$range": ["$start", "$end", "$step"]}, - error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject timestamp step", - ), -] - # Error: non-integral start NON_INTEGRAL_START_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -404,7 +141,7 @@ ] # Error: special numeric values -# Property [Special Numerics]: $map preserves NaN, Infinity, and boundary values. +# Property [Special Numerics]: $range rejects NaN and Infinity values. SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nan_start", @@ -592,7 +329,6 @@ ), ] -# Aggregate and test # Error: wrong arity ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -616,10 +352,7 @@ ] ALL_TESTS = ( - NON_NUMERIC_START_TESTS - + NON_NUMERIC_END_TESTS - + NON_NUMERIC_STEP_TESTS - + NON_INTEGRAL_START_TESTS + NON_INTEGRAL_START_TESTS + NON_INTEGRAL_END_TESTS + NON_INTEGRAL_STEP_TESTS + SPECIAL_NUMERIC_TESTS @@ -630,8 +363,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) -def test_range_error_insert(collection, test): - """Test $range error with values from inserted documents.""" +def test_range_value_error(collection, test): + """Test $range error with invalid numeric values.""" if test.doc is None: result = execute_expression(collection, test.expression) else: diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py index 8701e4743..999f43773 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py @@ -2,8 +2,7 @@ Core behavior tests for $zip expression. Tests zipping arrays of various element types, equal/unequal lengths, -useLongestLength, defaults, empty arrays, single arrays, nested arrays, -null propagation, and large arrays. +useLongestLength, and defaults. """ import pytest @@ -14,13 +13,12 @@ ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 assert_expression_result, - execute_expression, execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params # Success: basic zipping — equal length arrays -# Property [Basic Transform]: $map applies an expression to each element. +# Property [Basic Transform]: $zip transposes arrays element-wise. BASIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="two_int_arrays", @@ -223,241 +221,13 @@ ), ] -# Success: empty and single element -DEGENERATE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="both_empty", - doc={"arr0": [], "arr1": []}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=[], - msg="Should return empty for two empty arrays", - ), - ExpressionTestCase( - id="three_empty", - doc={"arr0": [], "arr1": [], "arr2": []}, - expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, - expected=[], - msg="Three empty arrays return []", - ), - ExpressionTestCase( - id="single_empty", - doc={"arr0": []}, - expression={"$zip": {"inputs": ["$arr0"]}}, - expected=[], - msg="Single empty array returns []", - ), - ExpressionTestCase( - id="single_element_each", - doc={"arr0": [1], "arr1": [10]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=[[1, 10]], - msg="Should zip single-element arrays", - ), - ExpressionTestCase( - id="single_input_array", - doc={"arr0": [1, 2, 3]}, - expression={"$zip": {"inputs": ["$arr0"]}}, - expected=[[1], [2], [3]], - msg="Single input should wrap each element", - ), - ExpressionTestCase( - id="all_single_element_three", - doc={"arr0": [1], "arr1": ["a"], "arr2": [True]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, - expected=[[1, "a", True]], - msg="All single-element arrays produce one row", - ), - ExpressionTestCase( - id="empty_with_longest_false", - doc={"arr0": [], "arr1": [1, 2, 3]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=[], - msg="Empty array with shortest length returns []", - ), - ExpressionTestCase( - id="empty_with_longest_true", - doc={"arr0": [], "arr1": [1, 2, 3]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, - expected=[[None, 1], [None, 2], [None, 3]], - msg="Empty array with longest length pads with null", - ), - ExpressionTestCase( - id="two_empty_longest_true", - doc={"arr0": [], "arr1": []}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, - expected=[], - msg="Two empty arrays with longest length return []", - ), -] - -# Success: nested arrays as elements -# Property [Nested Arrays]: $map operates on nested array structures. -NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="nested_arrays", - doc={"arr0": [[1, 2], [3, 4]], "arr1": ["a", "b"]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=[[[1, 2], "a"], [[3, 4], "b"]], - msg="Should zip nested arrays as elements", - ), - ExpressionTestCase( - id="objects_as_elements", - doc={"arr0": [{"x": 1}, {"x": 2}], "arr1": [{"y": 10}, {"y": 20}]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=[[{"x": 1}, {"y": 10}], [{"x": 2}, {"y": 20}]], - msg="Objects preserved as elements", - ), - ExpressionTestCase( - id="mixed_types_six", - doc={"arr0": [1, "a", None, True, {"k": 1}, [9]], "arr1": [10, 20, 30, 40, 50, 60]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=[[1, 10], ["a", 20], [None, 30], [True, 40], [{"k": 1}, 50], [[9], 60]], - msg="Mixed types preserved in transposition", - ), -] - -# Success: null propagation -NULL_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="null_first_input", - doc={"arr0": None, "arr1": [1, 2]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=None, - msg="Should return null when first input is null", - ), - ExpressionTestCase( - id="null_second_input", - doc={"arr0": [1, 2], "arr1": None}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=None, - msg="Should return null when second input is null", - ), - ExpressionTestCase( - id="all_null_inputs", - doc={"arr0": None, "arr1": None}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=None, - msg="Should return null when all inputs are null", - ), - ExpressionTestCase( - id="null_elements_in_arrays", - doc={"arr0": [1, None], "arr1": [None, 2]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=[[1, None], [None, 2]], - msg="Should preserve null elements within arrays", - ), -] - -# Success: objects as elements -OBJECT_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="arrays_of_objects", - doc={"arr0": [{"a": 1}], "arr1": [{"b": 2}]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=[[{"a": 1}, {"b": 2}]], - msg="Should zip arrays of objects", - ), -] - -# Success: large arrays -# Property [Large Arrays]: $map handles large arrays. -LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="large_arrays", - doc={"arr0": list(range(1000)), "arr1": list(range(1000, 2000))}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=[[i, i + 1000] for i in range(1000)], - msg="Should zip large arrays", - ), -] - -# Success: many input arrays -MANY_INPUTS_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="ten_inputs", - doc={f"arr{i}": [i] for i in range(10)}, - expression={"$zip": {"inputs": [f"$arr{i}" for i in range(10)]}}, - expected=[list(range(10))], - msg="Ten inputs transpose correctly", - ), - ExpressionTestCase( - id="fifty_inputs", - doc={f"arr{i}": [i] for i in range(50)}, - expression={"$zip": {"inputs": [f"$arr{i}" for i in range(50)]}}, - expected=[list(range(50))], - msg="50 single-element inputs produce one 50-element row", - ), -] - -# Success: multiple arrays of different lengths -MULTI_LENGTH_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - id="three_arrays_shortest", - doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, - expected=[[1, 10, 100]], - msg="Three arrays shortest = 1", - ), - ExpressionTestCase( - id="three_arrays_longest_no_defaults", - doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"], "useLongestLength": True}}, - expected=[ - [1, 10, 100], - [None, 20, 200], - [None, 30, 300], - [None, None, 400], - [None, None, 500], - ], - msg="Three arrays longest pads with null", - ), - ExpressionTestCase( - id="three_arrays_longest_with_defaults", - doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, - expression={ - "$zip": { - "inputs": ["$arr0", "$arr1", "$arr2"], - "useLongestLength": True, - "defaults": [0, "x", False], - } - }, - expected=[[1, 10, 100], [0, 20, 200], [0, 30, 300], [0, "x", 400], [0, "x", 500]], - msg="Three arrays longest with defaults", - ), - ExpressionTestCase( - id="four_arrays_two_empty", - doc={"arr0": [], "arr1": [], "arr2": [1, 2], "arr3": [3, 4, 5]}, - expression={ - "$zip": {"inputs": ["$arr0", "$arr1", "$arr2", "$arr3"], "useLongestLength": True} - }, - expected=[[None, None, 1, 3], [None, None, 2, 4], [None, None, None, 5]], - msg="Four arrays two empty with longest", - ), -] - -# Aggregate and test -ALL_TESTS = ( - BASIC_TESTS - + UNEQUAL_LENGTH_TESTS - + USE_LONGEST_TESTS - + DEFAULTS_TESTS - + DEGENERATE_TESTS - + NESTED_ARRAY_TESTS - + NULL_TESTS - + OBJECT_TESTS - + LARGE_ARRAY_TESTS - + MANY_INPUTS_TESTS - + MULTI_LENGTH_TESTS -) +ALL_TESTS = BASIC_TESTS + UNEQUAL_LENGTH_TESTS + USE_LONGEST_TESTS + DEFAULTS_TESTS @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) def test_zip_insert(collection, test): """Test $zip 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) + 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/zip/test_zip_degenerate_cases.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py new file mode 100644 index 000000000..d3737a04d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py @@ -0,0 +1,252 @@ +""" +Degenerate and edge case tests for $zip expression. + +Tests empty arrays, single arrays, nested arrays, null propagation, +objects as elements, large arrays, many inputs, and multi-length 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 ( # noqa: E501 + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Success: empty and single element +DEGENERATE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="both_empty", + doc={"arr0": [], "arr1": []}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=[], + msg="Should return empty for two empty arrays", + ), + ExpressionTestCase( + id="three_empty", + doc={"arr0": [], "arr1": [], "arr2": []}, + expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, + expected=[], + msg="Three empty arrays return []", + ), + ExpressionTestCase( + id="single_empty", + doc={"arr0": []}, + expression={"$zip": {"inputs": ["$arr0"]}}, + expected=[], + msg="Single empty array returns []", + ), + ExpressionTestCase( + id="single_element_each", + doc={"arr0": [1], "arr1": [10]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=[[1, 10]], + msg="Should zip single-element arrays", + ), + ExpressionTestCase( + id="single_input_array", + doc={"arr0": [1, 2, 3]}, + expression={"$zip": {"inputs": ["$arr0"]}}, + expected=[[1], [2], [3]], + msg="Single input should wrap each element", + ), + ExpressionTestCase( + id="all_single_element_three", + doc={"arr0": [1], "arr1": ["a"], "arr2": [True]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, + expected=[[1, "a", True]], + msg="All single-element arrays produce one row", + ), + ExpressionTestCase( + id="empty_with_longest_false", + doc={"arr0": [], "arr1": [1, 2, 3]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=[], + msg="Empty array with shortest length returns []", + ), + ExpressionTestCase( + id="empty_with_longest_true", + doc={"arr0": [], "arr1": [1, 2, 3]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, + expected=[[None, 1], [None, 2], [None, 3]], + msg="Empty array with longest length pads with null", + ), + ExpressionTestCase( + id="two_empty_longest_true", + doc={"arr0": [], "arr1": []}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, + expected=[], + msg="Two empty arrays with longest length return []", + ), +] + +# Success: nested arrays as elements +# Property [Nested Arrays]: $zip operates on nested array structures. +NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_arrays", + doc={"arr0": [[1, 2], [3, 4]], "arr1": ["a", "b"]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=[[[1, 2], "a"], [[3, 4], "b"]], + msg="Should zip nested arrays as elements", + ), + ExpressionTestCase( + id="objects_as_elements", + doc={"arr0": [{"x": 1}, {"x": 2}], "arr1": [{"y": 10}, {"y": 20}]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=[[{"x": 1}, {"y": 10}], [{"x": 2}, {"y": 20}]], + msg="Objects preserved as elements", + ), + ExpressionTestCase( + id="mixed_types_six", + doc={"arr0": [1, "a", None, True, {"k": 1}, [9]], "arr1": [10, 20, 30, 40, 50, 60]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=[[1, 10], ["a", 20], [None, 30], [True, 40], [{"k": 1}, 50], [[9], 60]], + msg="Mixed types preserved in transposition", + ), +] + +# Success: null propagation +NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="null_first_input", + doc={"arr0": None, "arr1": [1, 2]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=None, + msg="Should return null when first input is null", + ), + ExpressionTestCase( + id="null_second_input", + doc={"arr0": [1, 2], "arr1": None}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=None, + msg="Should return null when second input is null", + ), + ExpressionTestCase( + id="all_null_inputs", + doc={"arr0": None, "arr1": None}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=None, + msg="Should return null when all inputs are null", + ), + ExpressionTestCase( + id="null_elements_in_arrays", + doc={"arr0": [1, None], "arr1": [None, 2]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=[[1, None], [None, 2]], + msg="Should preserve null elements within arrays", + ), +] + +# Success: objects as elements +OBJECT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="arrays_of_objects", + doc={"arr0": [{"a": 1}], "arr1": [{"b": 2}]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=[[{"a": 1}, {"b": 2}]], + msg="Should zip arrays of objects", + ), +] + +# Success: large arrays +# Property [Large Arrays]: $zip handles large arrays. +LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="large_arrays", + doc={"arr0": list(range(1000)), "arr1": list(range(1000, 2000))}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + expected=[[i, i + 1000] for i in range(1000)], + msg="Should zip large arrays", + ), +] + +# Success: many input arrays +MANY_INPUTS_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="ten_inputs", + doc={f"arr{i}": [i] for i in range(10)}, + expression={"$zip": {"inputs": [f"$arr{i}" for i in range(10)]}}, + expected=[list(range(10))], + msg="Ten inputs transpose correctly", + ), + ExpressionTestCase( + id="fifty_inputs", + doc={f"arr{i}": [i] for i in range(50)}, + expression={"$zip": {"inputs": [f"$arr{i}" for i in range(50)]}}, + expected=[list(range(50))], + msg="50 single-element inputs produce one 50-element row", + ), +] + +# Success: multiple arrays of different lengths +MULTI_LENGTH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="three_arrays_shortest", + doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, + expected=[[1, 10, 100]], + msg="Three arrays shortest = 1", + ), + ExpressionTestCase( + id="three_arrays_longest_no_defaults", + doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"], "useLongestLength": True}}, + expected=[ + [1, 10, 100], + [None, 20, 200], + [None, 30, 300], + [None, None, 400], + [None, None, 500], + ], + msg="Three arrays longest pads with null", + ), + ExpressionTestCase( + id="three_arrays_longest_with_defaults", + doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, + expression={ + "$zip": { + "inputs": ["$arr0", "$arr1", "$arr2"], + "useLongestLength": True, + "defaults": [0, "x", False], + } + }, + expected=[[1, 10, 100], [0, 20, 200], [0, 30, 300], [0, "x", 400], [0, "x", 500]], + msg="Three arrays longest with defaults", + ), + ExpressionTestCase( + id="four_arrays_two_empty", + doc={"arr0": [], "arr1": [], "arr2": [1, 2], "arr3": [3, 4, 5]}, + expression={ + "$zip": {"inputs": ["$arr0", "$arr1", "$arr2", "$arr3"], "useLongestLength": True} + }, + expected=[[None, None, 1, 3], [None, None, 2, 4], [None, None, None, 5]], + msg="Four arrays two empty with longest", + ), +] + +ALL_TESTS = ( + DEGENERATE_TESTS + + NESTED_ARRAY_TESTS + + NULL_TESTS + + OBJECT_TESTS + + LARGE_ARRAY_TESTS + + MANY_INPUTS_TESTS + + MULTI_LENGTH_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_zip_degenerate(collection, test): + """Test $zip with degenerate and edge case inputs.""" + 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 184ec457a898dbc866297af7a026d8835c23beb5 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 8 Jul 2026 14:26:53 -0700 Subject: [PATCH 08/17] remove duplicate tests Signed-off-by: Alina (Xi) Li --- .../core/operator/expressions/array/map/test_smoke_map.py | 2 +- .../expressions/array/range/test_range_expressions.py | 7 ------- .../operator/expressions/array/range/test_smoke_range.py | 2 +- .../core/operator/expressions/array/zip/test_smoke_zip.py | 2 +- .../expressions/array/zip/test_zip_degenerate_cases.py | 7 ------- 5 files changed, 3 insertions(+), 17 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_map.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_map.py index 080fdcc07..d3ef08577 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_map.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_smoke_map.py @@ -38,4 +38,4 @@ def test_smoke_expression_map(collection): ) expected = [{"_id": 1, "doubled": [2, 4, 6]}, {"_id": 2, "doubled": [8, 10, 12]}] - assertSuccess(result, expected, "Should support $map expression") + assertSuccess(result, expected, "Should support $map expression", ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py index 19e201c1c..aec3b8c5d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py @@ -38,13 +38,6 @@ expected=[1, 2, 3], msg="Should resolve deeply nested field paths", ), - ExpressionTestCase( - id="step_from_field", - expression={"$range": ["$start", "$end", "$step"]}, - doc={"start": 0, "end": 10, "step": 3}, - expected=[0, 3, 6, 9], - msg="Should resolve step from field path", - ), ExpressionTestCase( id="nested_expr_start_end", expression={"$range": [{"$add": [1, 2]}, {"$multiply": [2, 5]}]}, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_range.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_range.py index 0291009f7..22bf631f0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_range.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_smoke_range.py @@ -26,4 +26,4 @@ def test_smoke_expression_range(collection): ) expected = [{"_id": 1, "sequence": [0, 1, 2, 3, 4]}, {"_id": 2, "sequence": [10, 11, 12]}] - assertSuccess(result, expected, "Should support $range expression") + assertSuccess(result, expected, "Should support $range expression", ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_zip.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_zip.py index 8cd4d56da..8e8b9d65a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_zip.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_smoke_zip.py @@ -28,4 +28,4 @@ def test_smoke_expression_zip(collection): ) expected = [{"_id": 1, "zipped": [[1, 10], [2, 20]]}, {"_id": 2, "zipped": [[3, 30], [4, 40]]}] - assertSuccess(result, expected, "Should support $zip expression") + assertSuccess(result, expected, "Should support $zip expression", ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py index d3737a04d..ce012f9cb 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py @@ -61,13 +61,6 @@ expected=[[1, "a", True]], msg="All single-element arrays produce one row", ), - ExpressionTestCase( - id="empty_with_longest_false", - doc={"arr0": [], "arr1": [1, 2, 3]}, - expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=[], - msg="Empty array with shortest length returns []", - ), ExpressionTestCase( id="empty_with_longest_true", doc={"arr0": [], "arr1": [1, 2, 3]}, From 3ecbae7df6d02af1bc617ba0a474e04d8c8036d0 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 8 Jul 2026 14:34:37 -0700 Subject: [PATCH 09/17] rename test names to be clear Signed-off-by: Alina (Xi) Li --- .../expressions/array/map/test_map_bson_types.py | 4 ++-- .../expressions/array/map/test_map_core_behavior.py | 6 +++--- .../operator/expressions/array/map/test_map_errors.py | 4 ++-- .../expressions/array/map/test_map_expressions.py | 4 ++-- .../expressions/array/range/test_range_boundary.py | 4 ++-- .../array/range/test_range_core_behavior.py | 4 ++-- .../expressions/array/range/test_range_expressions.py | 8 ++++---- .../array/range/test_range_numeric_acceptance.py | 2 +- .../array/zip/test_zip_argument_structure_errors.py | 2 +- .../expressions/array/zip/test_zip_bson_types.py | 10 +++++----- .../expressions/array/zip/test_zip_core_behavior.py | 4 ++-- .../expressions/array/zip/test_zip_degenerate_cases.py | 4 ++-- .../operator/expressions/array/zip/test_zip_errors.py | 4 ++-- .../expressions/array/zip/test_zip_expressions.py | 8 ++++---- 14 files changed, 34 insertions(+), 34 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py index ab9af5152..48bb35755 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py @@ -308,7 +308,7 @@ @pytest.mark.parametrize("test", pytest_params(ALL_BSON_TESTS)) -def test_map_bson_insert(collection, test): - """Test $map BSON types with values from inserted documents.""" +def test_map_bson_type_preservation(collection, test): + """Test $map preserves BSON types and handles type-specific transforms.""" 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/map/test_map_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py index 9c079edb7..43e31404c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py @@ -211,7 +211,7 @@ # Success: null element propagation -# Property [Null Propagation]: $zip returns null when inputs is null. +# Property [Null Propagation]: $map returns null when input is null. NULL_PROPAGATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="null_add_propagation", @@ -309,8 +309,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) -def test_map_insert(collection, test): - """Test $map with values from inserted documents.""" +def test_map_core_behavior(collection, test): + """Test $map core behavior: transforms, null propagation, conditionals.""" 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/map/test_map_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py index b6e0b3632..4eee2cb07 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py @@ -273,8 +273,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) -def test_map_not_array_insert(collection, test): - """Test $map error with non-array input from inserted documents.""" +def test_map_non_array_input_error(collection, test): + """Test $map rejects non-array input with correct error code.""" 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/map/test_map_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py index 0ebe3fb09..93f3e9aef 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py @@ -241,8 +241,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_EXPR_TESTS)) -def test_map_expression(collection, test): - """Test $map with field paths and expressions.""" +def test_map_field_paths_and_variables(collection, test): + """Test $map with field paths, $let, system variables, and nested composition.""" 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/range/test_range_boundary.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py index 5121dff0d..33f94a23d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py @@ -83,8 +83,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_BOUNDARY_TESTS)) -def test_range_boundary_insert(collection, test): - """Test $range boundary values with inserted documents.""" +def test_range_int32_boundary(collection, test): + """Test $range with INT32 boundary values for start, end, step.""" if test.doc is None: result = execute_expression(collection, test.expression) else: diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py index a14f37fc7..d3736d6a9 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py @@ -219,8 +219,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) -def test_range_insert(collection, test): - """Test $range with values from inserted documents.""" +def test_range_core_behavior(collection, test): + """Test $range core behavior: ascending, steps, descending, empty results.""" 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/range/test_range_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py index aec3b8c5d..2675108bb 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py @@ -22,7 +22,7 @@ from documentdb_tests.framework.parametrize import pytest_params # Field path lookups -# Property [Field Lookup]: $map resolves field paths in expressions. +# Property [Field Lookup]: $range resolves field paths in expressions. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_field_path", @@ -85,7 +85,7 @@ ] # Null/missing via expression — $range does NOT propagate null, it errors -# Property [Null/Missing Fields]: $map handles null and missing field paths. +# Property [Null/Missing Fields]: $range errors on null and missing field paths. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="missing_start_field", @@ -150,8 +150,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_EXPR_TESTS)) -def test_range_expression(collection, test): - """Test $range with field paths and expressions.""" +def test_range_field_paths_and_variables(collection, test): + """Test $range with field paths, $let, system variables, and null/missing errors.""" 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/range/test_range_numeric_acceptance.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py index 220f0e139..fdf849c9f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py @@ -217,7 +217,7 @@ @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) def test_range_numeric_acceptance(collection, test): - """Test $range with various accepted numeric types and value patterns.""" + """Test $range accepts Int64, whole doubles, whole Decimal128, and negative zero.""" 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/zip/test_zip_argument_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py index cbada117e..8356057a4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py @@ -71,7 +71,7 @@ ] # Non-object argument (error 34460) -# Property [Object Argument]: $map rejects non-object arguments. +# Property [Object Argument]: $zip rejects non-object arguments. NON_OBJECT_ARG_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="int_arg", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py index e3d972ff3..a21cff594 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py @@ -33,7 +33,7 @@ ) # BSON types preserved after zipping -# Property [Type Preservation]: $map preserves each element's BSON type. +# Property [Type Preservation]: $zip preserves each element's BSON type. BSON_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="int64_values", @@ -117,7 +117,7 @@ ] # Mixed BSON types across arrays -# Property [Mixed Types]: $map processes arrays with mixed BSON types. +# Property [Mixed Types]: $zip processes arrays with mixed BSON types. MIXED_BSON_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="mixed_bson_types", @@ -129,7 +129,7 @@ ] # Special numeric values as elements -# Property [Special Numerics]: $map preserves NaN, Infinity, and boundary values. +# Property [Special Numerics]: $zip preserves NaN, Infinity, and boundary values. SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="infinity_values", @@ -388,8 +388,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_BSON_TESTS)) -def test_zip_bson_insert(collection, test): - """Test $zip BSON types with values from inserted documents.""" +def test_zip_bson_type_preservation(collection, test): + """Test $zip preserves BSON types in zipped output arrays.""" if test.doc is None: result = execute_expression(collection, test.expression) else: diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py index 999f43773..dd7a13962 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py @@ -225,8 +225,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) -def test_zip_insert(collection, test): - """Test $zip with values from inserted documents.""" +def test_zip_core_behavior(collection, test): + """Test $zip core behavior: basic zipping, unequal lengths, useLongestLength, defaults.""" 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/zip/test_zip_degenerate_cases.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py index ce012f9cb..892e521a8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py @@ -234,8 +234,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) -def test_zip_degenerate(collection, test): - """Test $zip with degenerate and edge case inputs.""" +def test_zip_edge_cases(collection, test): + """Test $zip with empty, single, nested, null, large, and multi-length inputs.""" if test.doc is None: result = execute_expression(collection, test.expression) else: diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py index 4c90212bb..173086dd4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py @@ -430,8 +430,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_INPUT_ELEMENT_TESTS)) -def test_zip_not_array_insert(collection, test): - """Test $zip error with non-array input element from inserted documents.""" +def test_zip_non_array_input_error(collection, test): + """Test $zip rejects non-array inputs and invalid useLongestLength/defaults.""" if test.doc is None: result = execute_expression(collection, test.expression) else: diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py index 7d243c952..f658d4b97 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py @@ -18,7 +18,7 @@ from documentdb_tests.framework.parametrize import pytest_params # Field path lookups -# Property [Field Lookup]: $map resolves field paths in expressions. +# Property [Field Lookup]: $zip resolves field paths in expressions. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_field_path", @@ -99,7 +99,7 @@ ] # Null/missing via expression -# Property [Null/Missing Fields]: $map handles null and missing field paths. +# Property [Null/Missing Fields]: $zip handles null and missing field paths. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="missing_field", @@ -232,8 +232,8 @@ @pytest.mark.parametrize("test", pytest_params(ALL_EXPR_TESTS)) -def test_zip_expression(collection, test): - """Test $zip with field paths and expressions.""" +def test_zip_field_paths_and_variables(collection, test): + """Test $zip with field paths, $let, system variables, and null propagation.""" 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 1a92cbdbe7440fbb0f65a31dcd76276f38be69ce Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Wed, 8 Jul 2026 15:09:33 -0700 Subject: [PATCH 10/17] fix styles Signed-off-by: Alina (Xi) Li --- .../array/map/test_map_as_errors.py | 65 +++--- .../array/map/test_map_bson_types.py | 103 +++++---- .../array/map/test_map_core_behavior.py | 144 ++++++------ .../expressions/array/map/test_map_errors.py | 131 ++++++----- .../array/map/test_map_expressions.py | 75 +++---- .../array/map/test_map_structure_errors.py | 50 ++--- .../array/range/test_range_boundary.py | 31 ++- .../array/range/test_range_core_behavior.py | 112 +++++----- .../array/range/test_range_expressions.py | 62 +++--- .../range/test_range_numeric_acceptance.py | 106 ++++----- .../array/range/test_range_type_errors.py | 146 ++++++------ .../array/range/test_range_value_errors.py | 177 ++++++++------- .../zip/test_zip_argument_structure_errors.py | 61 +++-- .../array/zip/test_zip_bson_types.py | 131 ++++++----- .../array/zip/test_zip_core_behavior.py | 91 ++++---- .../array/zip/test_zip_degenerate_cases.py | 106 ++++----- .../expressions/array/zip/test_zip_errors.py | 209 +++++++++--------- .../array/zip/test_zip_expressions.py | 101 +++++---- .../test_expressions_combination_map.py | 18 +- .../test_expressions_combination_range.py | 28 +-- .../test_expressions_combination_zip.py | 36 +-- 21 files changed, 982 insertions(+), 1001 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py index 94fcacb8c..91c555023 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py @@ -19,102 +19,103 @@ from documentdb_tests.framework.error_codes import FAILED_TO_PARSE_ERROR from documentdb_tests.framework.parametrize import pytest_params +# Property [Invalid As Type]: $map rejects non-string types for the as parameter. INVALID_AS_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="type_int", + "type_int", expression={"$map": {"input": [1, 2, 3], "as": 1, "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=int should error", + msg="$map should reject int as variable name", ), ExpressionTestCase( - id="type_long", + "type_long", expression={"$map": {"input": [1, 2, 3], "as": Int64(1), "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=Int64 should error", + msg="$map should reject Int64 as variable name", ), ExpressionTestCase( - id="type_object", + "type_object", expression={"$map": {"input": [1, 2, 3], "as": {"a": 1}, "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=object should error", + msg="$map should reject object as variable name", ), ExpressionTestCase( - id="type_array", + "type_array", expression={"$map": {"input": [1, 2, 3], "as": [1], "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=array should error", + msg="$map should reject array as variable name", ), ExpressionTestCase( - id="type_minkey", + "type_minkey", expression={"$map": {"input": [1, 2, 3], "as": MinKey(), "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=MinKey should error", + msg="$map should reject MinKey as variable name", ), ExpressionTestCase( - id="type_maxkey", + "type_maxkey", expression={"$map": {"input": [1, 2, 3], "as": MaxKey(), "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=MaxKey should error", + msg="$map should reject MaxKey as variable name", ), ExpressionTestCase( - id="type_bindata", + "type_bindata", expression={"$map": {"input": [1, 2, 3], "as": Binary(b"\x00", 0), "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=Binary should error", + msg="$map should reject Binary as variable name", ), ExpressionTestCase( - id="type_objectid", + "type_objectid", expression={"$map": {"input": [1, 2, 3], "as": ObjectId(), "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=ObjectId should error", + msg="$map should reject ObjectId as variable name", ), ExpressionTestCase( - id="type_date", + "type_date", expression={"$map": {"input": [1, 2, 3], "as": datetime(2026, 1, 1), "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=datetime should error", + msg="$map should reject datetime as variable name", ), ExpressionTestCase( - id="type_timestamp", + "type_timestamp", expression={"$map": {"input": [1, 2, 3], "as": Timestamp(0, 0), "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=Timestamp should error", + msg="$map should reject Timestamp as variable name", ), ExpressionTestCase( - id="type_regex", + "type_regex", expression={"$map": {"input": [1, 2, 3], "as": Regex("pattern"), "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=Regex should error", + msg="$map should reject Regex as variable name", ), ExpressionTestCase( - id="type_bool_true", + "type_bool_true", expression={"$map": {"input": [1, 2, 3], "as": True, "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=True should error", + msg="$map should reject boolean as variable name", ), ExpressionTestCase( - id="type_null", + "type_null", expression={"$map": {"input": [1, 2, 3], "as": None, "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=None should error", + msg="$map should reject null as variable name", ), ExpressionTestCase( - id="type_empty_string", + "type_empty_string", expression={"$map": {"input": [1, 2, 3], "as": "", "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as='' should error", + msg="$map should reject empty string as variable name", ), ExpressionTestCase( - id="type_nan", + "type_nan", expression={"$map": {"input": [1, 2, 3], "as": float("nan"), "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=NaN should error", + msg="$map should reject NaN as variable name", ), ExpressionTestCase( - id="type_infinity", + "type_infinity", expression={"$map": {"input": [1, 2, 3], "as": float("inf"), "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, - msg="as=inf should error", + msg="$map should reject Infinity as variable name", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py index 48bb35755..2fb12b020 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py @@ -33,25 +33,25 @@ INT64_MIN, ) -# BSON types preserved via identity map +# Property [Type Preservation]: $map preserves each element BSON type. # Property [Type Preservation]: $map preserves each element's BSON type. BSON_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int64_values", + "int64_values", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [Int64(1), Int64(2), Int64(3)]}, expected=[Int64(1), Int64(2), Int64(3)], - msg="Should preserve Int64 values", + msg="$map should preserve Int64 values", ), ExpressionTestCase( - id="decimal128_values", + "decimal128_values", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [Decimal128("1.5"), Decimal128("2.5")]}, expected=[Decimal128("1.5"), Decimal128("2.5")], - msg="Should preserve Decimal128 values", + msg="$map should preserve Decimal128 values", ), ExpressionTestCase( - id="datetime_values", + "datetime_values", expression={"$map": {"input": "$arr", "in": "$$this"}}, 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="$map should preserve datetime values", ), ExpressionTestCase( - id="objectid_values", + "objectid_values", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")]}, expected=[ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")], - msg="Should preserve ObjectId values", + msg="$map should preserve ObjectId values", ), ExpressionTestCase( - id="binary_values", + "binary_values", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [Binary(b"\x01", 0), Binary(b"\x02", 0)]}, expected=[b"\x01", b"\x02"], - msg="Should preserve Binary values", + msg="$map should preserve Binary values", ), ExpressionTestCase( - id="binary_subtype_preservation", + "binary_subtype_preservation", expression={"$map": {"input": "$arr", "in": "$$this"}}, 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="$map should preserve Binary subtype", ), ExpressionTestCase( - id="regex_values", + "regex_values", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [Regex("^a", "i"), Regex("^b", "i")]}, expected=[Regex("^a", "i"), Regex("^b", "i")], - msg="Should preserve Regex values", + msg="$map should preserve Regex values", ), ExpressionTestCase( - id="timestamp_values", + "timestamp_values", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [Timestamp(1, 0), Timestamp(2, 0)]}, expected=[Timestamp(1, 0), Timestamp(2, 0)], - msg="Should preserve Timestamp values", + msg="$map should preserve Timestamp values", ), ExpressionTestCase( - id="minkey_maxkey", + "minkey_maxkey", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [MinKey(), MaxKey()]}, expected=[MinKey(), MaxKey()], - msg="Should preserve MinKey/MaxKey values", + msg="$map should preserve MinKey/MaxKey values", ), ExpressionTestCase( - id="uuid_values", + "uuid_values", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={ "arr": [ @@ -120,22 +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="$map should preserve UUID binary values", ), ] -# Mixed BSON types # Property [Mixed Types]: $map processes arrays with mixed BSON types. MIXED_BSON_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="mixed_bson_types", + "mixed_bson_types", expression={"$map": {"input": "$arr", "in": "$$this"}}, 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 via identity", + msg="$map should preserve mixed BSON types via identity", ), ExpressionTestCase( - id="mixed_dates_and_ids", + "mixed_dates_and_ids", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={ "arr": [ @@ -149,87 +148,86 @@ ObjectId("000000000000000000000001"), Timestamp(1, 0), ], - msg="Should preserve dates, ObjectIds, timestamps", + msg="$map should preserve dates, ObjectIds, timestamps", ), ] -# Special numeric values as elements # Property [Special Numerics]: $map preserves NaN, Infinity, and boundary values. SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="infinity_values", + "infinity_values", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY]}, expected=[FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY], - msg="Should preserve infinity values", + msg="$map should preserve infinity values", ), ExpressionTestCase( - id="decimal128_infinity", + "decimal128_infinity", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY]}, expected=[DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY], - msg="Should preserve Decimal128 infinity values", + msg="$map should preserve Decimal128 infinity values", ), ExpressionTestCase( - id="boundary_values", + "boundary_values", expression={"$map": {"input": "$arr", "in": "$$this"}}, 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="$map should preserve numeric boundary values", ), ExpressionTestCase( - id="negative_zero", + "negative_zero", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [DOUBLE_NEGATIVE_ZERO, DECIMAL128_NEGATIVE_ZERO]}, expected=[DOUBLE_NEGATIVE_ZERO, DECIMAL128_NEGATIVE_ZERO], - msg="Should preserve negative zero values", + msg="$map should preserve negative zero values", ), ] -# Decimal128 precision preservation +# Property [Decimal128 Precision]: $map preserves Decimal128 trailing zeros and precision. DECIMAL128_PRECISION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="decimal128_trailing_zeros", + "decimal128_trailing_zeros", expression={"$map": {"input": "$arr", "in": "$$this"}}, 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="$map decimal128 trailing zeros preserved", ), ExpressionTestCase( - id="decimal128_nan", + "decimal128_nan", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [DECIMAL128_NAN, Decimal128("1")]}, expected=[DECIMAL128_NAN, Decimal128("1")], - msg="Decimal128 NaN preserved", + msg="$map decimal128 NaN preserved", ), ] -# BSON type transformations +# Property [Type Transforms]: $map applies type-specific operator transformations. # Property [Type Transform]: $map transforms elements using type-specific operations. BSON_TRANSFORM_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="multiply_int64", + "multiply_int64", expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", Int64(2)]}}}, doc={"arr": [Int64(10), Int64(20), Int64(30)]}, expected=[Int64(20), Int64(40), Int64(60)], msg="$multiply on Int64 should preserve Int64 type", ), ExpressionTestCase( - id="add_decimal128", + "add_decimal128", expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", Decimal128("0.1")]}}}, doc={"arr": [Decimal128("1.5"), Decimal128("2.5"), Decimal128("3.5")]}, expected=[Decimal128("1.6"), Decimal128("2.6"), Decimal128("3.6")], msg="$add on Decimal128 should preserve precision", ), ExpressionTestCase( - id="type_of_mixed_bson", + "type_of_mixed_bson", expression={"$map": {"input": "$arr", "in": {"$type": "$$this"}}}, doc={"arr": [1, "two", Int64(3), Decimal128("4"), True, None]}, expected=["int", "string", "long", "decimal", "bool", "null"], msg="$type on mixed BSON types", ), ExpressionTestCase( - id="dateToString_datetime", + "dateToString_datetime", expression={ "$map": { "input": "$arr", @@ -246,35 +244,35 @@ msg="$dateToString on datetime array", ), ExpressionTestCase( - id="toString_objectid", + "toString_objectid", expression={"$map": {"input": "$arr", "in": {"$toString": "$$this"}}}, doc={"arr": [ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")]}, expected=["000000000000000000000001", "000000000000000000000002"], msg="$toString on ObjectId array", ), ExpressionTestCase( - id="toLong_int_values", + "toLong_int_values", expression={"$map": {"input": "$arr", "in": {"$toLong": "$$this"}}}, doc={"arr": [1, 2, 3]}, expected=[Int64(1), Int64(2), Int64(3)], msg="$toLong converts int to Int64", ), ExpressionTestCase( - id="toDouble_decimal128", + "toDouble_decimal128", expression={"$map": {"input": "$arr", "in": {"$toDouble": "$$this"}}}, doc={"arr": [Decimal128("1.5"), Decimal128("2.0")]}, expected=[1.5, 2.0], msg="$toDouble on Decimal128 array", ), ExpressionTestCase( - id="concat_strings", + "concat_strings", expression={"$map": {"input": "$arr", "in": {"$concat": ["$$this", "!"]}}}, doc={"arr": ["hello", "world"]}, expected=["hello!", "world!"], msg="$concat on string array", ), ExpressionTestCase( - id="add_millis_to_datetime", + "add_millis_to_datetime", expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 86400000]}}}, doc={ "arr": [ @@ -286,10 +284,10 @@ datetime(2024, 1, 2, tzinfo=timezone.utc), datetime(2024, 6, 2, tzinfo=timezone.utc), ], - msg="Add one day in millis to datetime array", + msg="$map add one day in millis to datetime array", ), ExpressionTestCase( - id="subtract_int64", + "subtract_int64", expression={"$map": {"input": "$arr", "in": {"$subtract": ["$$this", Int64(50)]}}}, doc={"arr": [Int64(100), Int64(200), Int64(300)]}, expected=[Int64(50), Int64(150), Int64(250)], @@ -297,7 +295,6 @@ ), ] -# Aggregate and test ALL_BSON_TESTS = ( BSON_TYPE_TESTS + MIXED_BSON_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py index 43e31404c..5f1155939 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py @@ -17,236 +17,235 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# Success: basic mapping +# Property [Basic Transform]: $map applies an expression to each array element. # Property [Basic Transform]: $map applies an expression to each element. BASIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="multiply_each", + "multiply_each", expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, doc={"arr": [1, 2, 3]}, expected=[2, 4, 6], - msg="Should multiply each element by 2", + msg="$map should multiply each element by 2", ), ExpressionTestCase( - id="add_each", + "add_each", expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 5]}}}, doc={"arr": [10, 20, 30]}, expected=[15, 25, 35], - msg="Should add 5 to each element", + msg="$map should add 5 to each element", ), ExpressionTestCase( - id="identity", + "identity", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [1, 2, 3]}, expected=[1, 2, 3], - msg="Identity transform should return same array", + msg="$map identity transform should return same array", ), ExpressionTestCase( - id="constant_in", + "constant_in", expression={"$map": {"input": "$arr", "in": 13}}, doc={"arr": [1, 2, 3]}, expected=[13, 13, 13], - msg="Constant in expression should repeat for each element", + msg="$map constant in expression should repeat for each element", ), ExpressionTestCase( - id="string_elements", + "string_elements", expression={"$map": {"input": "$arr", "in": {"$toUpper": "$$this"}}}, doc={"arr": ["a", "b", "c"]}, expected=["A", "B", "C"], - msg="Should uppercase each string element", + msg="$map should uppercase each string element", ), ExpressionTestCase( - id="bool_elements", + "bool_elements", expression={"$map": {"input": "$arr", "in": {"$not": "$$this"}}}, doc={"arr": [True, False, True]}, expected=[False, True, False], - msg="Should negate each boolean element", + msg="$map should negate each boolean element", ), ExpressionTestCase( - id="single_element", + "single_element", expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 1]}}}, doc={"arr": [42]}, expected=[43], - msg="Should map single element", + msg="$map should map single element", ), ExpressionTestCase( - id="empty_array", + "empty_array", expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, doc={"arr": []}, expected=[], - msg="Should return empty array for empty input", + msg="$map should return empty array for empty input", ), ExpressionTestCase( - id="null_input", + "null_input", expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, doc={"arr": None}, expected=None, - msg="Should return null when input is null", + msg="$map should return null when input is null", ), ExpressionTestCase( - id="custom_as_var", + "custom_as_var", expression={"$map": {"input": "$arr", "as": "val", "in": {"$multiply": ["$$val", 3]}}}, doc={"arr": [1, 2, 3]}, expected=[3, 6, 9], - msg="Should use custom 'as' variable name", + msg="$map should use custom 'as' variable name", ), ] -# Success: nested arrays (map does not flatten) +# Property [Nested Arrays]: $map does not flatten nested array structures. # Property [Nested Arrays]: $map operates on nested array structures. NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_arrays_identity", + "nested_arrays_identity", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [[1, 2], [3, 4]]}, expected=[[1, 2], [3, 4]], - msg="Should preserve nested arrays", + msg="$map should preserve nested arrays", ), ExpressionTestCase( - id="nested_arrays_size", + "nested_arrays_size", expression={"$map": {"input": "$arr", "in": {"$size": "$$this"}}}, doc={"arr": [[1, 2], [3, 4, 5], []]}, expected=[2, 3, 0], - msg="Should compute size of each subarray", + msg="$map should compute size of each subarray", ), ExpressionTestCase( - id="extract_field_from_objects", + "extract_field_from_objects", expression={"$map": {"input": "$arr", "in": "$$this.a"}}, doc={"arr": [{"a": 1, "b": 2}, {"a": 3, "b": 4}]}, expected=[1, 3], - msg="Should extract field from each object element", + msg="$map should extract field from each object element", ), ] -# Success: elements with null +# Property [Null Elements]: $map preserves null elements within the array. NULL_ELEMENT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="null_elements_identity", + "null_elements_identity", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": [None, 1, None]}, expected=[None, 1, None], - msg="Should preserve null elements", + msg="$map should preserve null elements", ), ExpressionTestCase( - id="null_elements_ifnull", + "null_elements_ifnull", expression={"$map": {"input": "$arr", "in": {"$ifNull": ["$$this", 0]}}}, doc={"arr": [None, 1, None]}, expected=[0, 1, 0], - msg="Should replace null elements with $ifNull", + msg="$map should replace null elements with $ifNull", ), ] -# Success: wrap each element +# Property [Element Wrapping]: $map wraps each element in the specified structure. WRAP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="wrap_in_array", + "wrap_in_array", expression={"$map": {"input": "$arr", "in": ["$$this"]}}, doc={"arr": [1, 2, 3]}, expected=[[1], [2], [3]], - msg="Should wrap each element in an array", + msg="$map should wrap each element in an array", ), ExpressionTestCase( - id="wrap_in_object", + "wrap_in_object", expression={"$map": {"input": "$arr", "in": {"val": "$$this"}}}, doc={"arr": [1, 2, 3]}, expected=[{"val": 1}, {"val": 2}, {"val": 3}], - msg="Should wrap each element in an object", + msg="$map should wrap each element in an object", ), ] -# Success: type conversion in expression +# Property [Type Conversion]: $map applies type conversion expressions to each element. TYPE_CONVERSION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="to_string", + "to_string", expression={"$map": {"input": "$arr", "in": {"$toString": "$$this"}}}, doc={"arr": [1, 2, 3]}, expected=["1", "2", "3"], - msg="Should convert each element to string", + msg="$map should convert each element to string", ), ExpressionTestCase( - id="type_of_each", + "type_of_each", expression={"$map": {"input": "$arr", "in": {"$type": "$$this"}}}, doc={"arr": [1, "two", True, None]}, expected=["int", "string", "bool", "null"], - msg="Should return type of each element", + msg="$map should return type of each element", ), ] -# Success: large array +# Property [Large Arrays]: $map handles arrays with many elements. # Property [Large Arrays]: $map handles large arrays. LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="large_array_1000", + "large_array_1000", expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, doc={"arr": list(range(1000))}, expected=[i * 2 for i in range(1000)], - msg="Should map over 1000 elements", + msg="$map should map over 1000 elements", ), ] -# Success: order preservation and duplicates +# Property [Order Preservation]: $map preserves element order and duplicates. ORDER_DUPLICATE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="preserves_order", + "preserves_order", expression={"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 10]}}}, doc={"arr": [3, 1, 4, 1, 5, 9]}, expected=[30, 10, 40, 10, 50, 90], - msg="Should preserve element order", + msg="$map should preserve element order", ), ExpressionTestCase( - id="duplicate_elements", + "duplicate_elements", expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 10]}}}, doc={"arr": [1, 1, 1, 1]}, expected=[11, 11, 11, 11], - msg="Should process all duplicate elements", + msg="$map should process all duplicate elements", ), ExpressionTestCase( - id="duplicate_nulls", + "duplicate_nulls", expression={"$map": {"input": "$arr", "in": {"$ifNull": ["$$this", 0]}}}, doc={"arr": [None, None, None]}, expected=[0, 0, 0], - msg="Should process all null duplicates", + msg="$map should process all null duplicates", ), ] -# Success: null element propagation # Property [Null Propagation]: $map returns null when input is null. NULL_PROPAGATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="null_add_propagation", + "null_add_propagation", expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 1]}}}, doc={"arr": [None, None]}, expected=[None, None], - msg="Null + number should propagate null", + msg="$map null + number should propagate null", ), ExpressionTestCase( - id="null_in_mixed_add", + "null_in_mixed_add", expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", 1]}}}, doc={"arr": [1, 2, None, 4]}, expected=[2, 3, None, 5], - msg="Null element should propagate, others should add", + msg="$map null element should propagate, others should add", ), ExpressionTestCase( - id="null_input_ignores_in", + "null_input_ignores_in", expression={"$map": {"input": "$arr", "in": {"$add": [2, 1]}}}, doc={"arr": None}, expected=None, - msg="Null input returns null regardless of in expression", + msg="$map null input returns null regardless of in expression", ), ExpressionTestCase( - id="null_element_in_ignores", + "null_element_in_ignores", expression={"$map": {"input": "$arr", "in": {"$add": [2, 1]}}}, doc={"arr": [None]}, expected=[3], - msg="Expression ignoring element should still produce result", + msg="$map expression ignoring element should still produce result", ), ] -# Success: conditional in expressions +# Property [Conditional Expressions]: $map supports conditional logic in the in expression. CONDITIONAL_IN_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="cond_classify", + "cond_classify", expression={ "$map": {"input": "$arr", "in": {"$cond": [{"$gte": ["$$this", 3]}, "high", "low"]}} }, @@ -255,7 +254,7 @@ msg="$cond should classify elements", ), ExpressionTestCase( - id="cond_isarray", + "cond_isarray", expression={ "$map": { "input": "$arr", @@ -268,32 +267,31 @@ ), ] -# Success: complex nested in expressions +# Property [Nested Expressions]: $map supports deeply nested operator expressions. NESTED_IN_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_multiply_add", + "nested_multiply_add", expression={"$map": {"input": "$arr", "in": {"$multiply": [{"$add": ["$$this", 1]}, 2]}}}, doc={"arr": [1, 2, 3]}, expected=[4, 6, 8], - msg="Nested expression should work", + msg="$map nested expression should work", ), ExpressionTestCase( - id="self_reference_double", + "self_reference_double", expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", "$$this"]}}}, doc={"arr": [1, 2, 3]}, expected=[2, 4, 6], - msg="Self-reference should double each element", + msg="$map self-reference should double each element", ), ExpressionTestCase( - id="produce_nested_arrays", + "produce_nested_arrays", expression={"$map": {"input": "$arr", "in": ["$$this", {"$multiply": ["$$this", 2]}]}}, doc={"arr": [1, 2, 3]}, expected=[[1, 2], [2, 4], [3, 6]], - msg="Should produce nested arrays", + msg="$map should produce nested arrays", ), ] -# Aggregate and test ALL_TESTS = ( BASIC_TESTS + NESTED_ARRAY_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py index 4eee2cb07..2750c6fee 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py @@ -39,236 +39,235 @@ INT64_MIN, ) -# Error: non-array input — standard BSON types +# Property [Non-Array Input]: $map rejects non-array input with all BSON types. NOT_ARRAY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="string_input", + "string_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": "hello"}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject string input", + msg="$map should reject string input", ), ExpressionTestCase( - id="int_input", + "int_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": 42}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject int input", + msg="$map should reject int input", ), ExpressionTestCase( - id="negative_int_input", + "negative_int_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": -42}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject negative int input", + msg="$map should reject negative int input", ), ExpressionTestCase( - id="bool_input", + "bool_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": True}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject bool input", + msg="$map should reject bool input", ), ExpressionTestCase( - id="object_input", + "object_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": {"a": 1}}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject object input", + msg="$map should reject object input", ), ExpressionTestCase( - id="double_input", + "double_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": 3.14}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject double input", + msg="$map should reject double input", ), ExpressionTestCase( - id="negative_double_input", + "negative_double_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": -3.14}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject negative double input", + msg="$map should reject negative double input", ), ExpressionTestCase( - id="decimal128_input", + "decimal128_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": Decimal128("1")}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject decimal128 input", + msg="$map should reject decimal128 input", ), ExpressionTestCase( - id="int64_input", + "int64_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": Int64(1)}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject int64 input", + msg="$map should reject int64 input", ), ExpressionTestCase( - id="objectid_input", + "objectid_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": ObjectId()}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject objectid input", + msg="$map should reject objectid input", ), ExpressionTestCase( - id="datetime_input", + "datetime_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": datetime(2024, 1, 1, tzinfo=timezone.utc)}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject datetime input", + msg="$map should reject datetime input", ), ExpressionTestCase( - id="binary_input", + "binary_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": Binary(b"x", 0)}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject binary input", + msg="$map should reject binary input", ), ExpressionTestCase( - id="regex_input", + "regex_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": Regex("x")}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject regex input", + msg="$map should reject regex input", ), ExpressionTestCase( - id="maxkey_input", + "maxkey_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": MaxKey()}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject maxkey input", + msg="$map should reject maxkey input", ), ExpressionTestCase( - id="minkey_input", + "minkey_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": MinKey()}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject minkey input", + msg="$map should reject minkey input", ), ExpressionTestCase( - id="timestamp_input", + "timestamp_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": Timestamp(0, 0)}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject timestamp input", + msg="$map should reject timestamp input", ), ] -# Error: special float/Decimal128 values +# Property [Special Numeric Input]: $map rejects special numeric values as input. SPECIAL_NUMERIC_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nan_input", + "nan_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": FLOAT_NAN}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject NaN input", + msg="$map should reject NaN input", ), ExpressionTestCase( - id="inf_input", + "inf_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": FLOAT_INFINITY}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject Infinity input", + msg="$map should reject Infinity input", ), ExpressionTestCase( - id="neg_inf_input", + "neg_inf_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": FLOAT_NEGATIVE_INFINITY}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject -Infinity input", + msg="$map should reject -Infinity input", ), ExpressionTestCase( - id="neg_zero_input", + "neg_zero_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": DOUBLE_NEGATIVE_ZERO}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject negative zero input", + msg="$map should reject negative zero input", ), ExpressionTestCase( - id="decimal128_nan_input", + "decimal128_nan_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": DECIMAL128_NAN}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject Decimal128 NaN input", + msg="$map should reject Decimal128 NaN input", ), ExpressionTestCase( - id="decimal128_neg_nan_input", + "decimal128_neg_nan_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": Decimal128("-NaN")}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject Decimal128 -NaN input", + msg="$map should reject Decimal128 -NaN input", ), ExpressionTestCase( - id="decimal128_inf_input", + "decimal128_inf_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": DECIMAL128_INFINITY}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject Decimal128 Infinity input", + msg="$map should reject Decimal128 Infinity input", ), ExpressionTestCase( - id="decimal128_neg_inf_input", + "decimal128_neg_inf_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": DECIMAL128_NEGATIVE_INFINITY}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject Decimal128 -Infinity input", + msg="$map should reject Decimal128 -Infinity input", ), ExpressionTestCase( - id="decimal128_neg_zero_input", + "decimal128_neg_zero_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": DECIMAL128_NEGATIVE_ZERO}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject Decimal128 -0 input", + msg="$map should reject Decimal128 -0 input", ), ] -# Error: numeric boundary values +# Property [Boundary Input]: $map rejects numeric boundary values as input. BOUNDARY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int32_max_input", + "int32_max_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": INT32_MAX}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject INT32_MAX input", + msg="$map should reject INT32_MAX input", ), ExpressionTestCase( - id="int32_min_input", + "int32_min_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": INT32_MIN}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject INT32_MIN input", + msg="$map should reject INT32_MIN input", ), ExpressionTestCase( - id="int64_max_input", + "int64_max_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": INT64_MAX}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject INT64_MAX input", + msg="$map should reject INT64_MAX input", ), ExpressionTestCase( - id="int64_min_input", + "int64_min_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": INT64_MIN}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject INT64_MIN input", + msg="$map should reject INT64_MIN input", ), ExpressionTestCase( - id="decimal128_max_input", + "decimal128_max_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": DECIMAL128_MAX}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject DECIMAL128_MAX input", + msg="$map should reject DECIMAL128_MAX input", ), ExpressionTestCase( - id="decimal128_min_input", + "decimal128_min_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, doc={"arr": DECIMAL128_MIN}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, - msg="Should reject DECIMAL128_MIN input", + msg="$map should reject DECIMAL128_MIN input", ), ] -# Aggregate and test ALL_TESTS = NOT_ARRAY_ERROR_TESTS + SPECIAL_NUMERIC_ERROR_TESTS + BOUNDARY_ERROR_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py index 93f3e9aef..b841a2345 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py @@ -16,65 +16,64 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# Field path lookups +# Property [Field Path Resolution]: $map resolves nested and composite field paths. # Property [Field Lookup]: $map resolves field paths in expressions. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_field_path", + "nested_field_path", expression={"$map": {"input": "$a.b", "in": {"$multiply": ["$$this", 2]}}}, doc={"a": {"b": [1, 2, 3]}}, expected=[2, 4, 6], - msg="Should resolve nested field path", + msg="$map should resolve nested field path", ), ExpressionTestCase( - id="deeply_nested_field", + "deeply_nested_field", expression={"$map": {"input": "$a.b.c", "in": "$$this"}}, doc={"a": {"b": {"c": [10, 20]}}}, expected=[10, 20], - msg="Should resolve deeply nested field path", + msg="$map should resolve deeply nested field path", ), ExpressionTestCase( - id="composite_array_path", + "composite_array_path", expression={"$map": {"input": "$a.b", "in": {"$multiply": ["$$this", 10]}}}, doc={"a": [{"b": 1}, {"b": 2}, {"b": 3}]}, expected=[10, 20, 30], - msg="Composite array path should resolve to array", + msg="$map composite array path should resolve to array", ), ExpressionTestCase( - id="index_path_on_object_key", + "index_path_on_object_key", expression={"$map": {"input": "$a.0.b", "in": "$$this"}}, doc={"a": {"0": {"b": [1, 2, 3]}}}, expected=[1, 2, 3], - msg="Object key '0' resolves correctly", + msg="$map object key '0' resolves correctly", ), ExpressionTestCase( - id="object_key_zero", + "object_key_zero", expression={"$map": {"input": "$a.0", "in": "$$this"}}, 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={"$map": {"input": "$arr", "in": {"$add": ["$$this", "$val"]}}}, doc={"arr": [1, 2, 3], "val": 100}, expected=[101, 102, 103], - msg="Should access outer document field in 'in' expression", + msg="$map should access outer document field in 'in' expression", ), ExpressionTestCase( - id="array_expression_input", + "array_expression_input", expression={"$map": {"input": ["$x", "$y"], "in": {"$multiply": ["$$this", 2]}}}, doc={"x": 1, "y": 2}, expected=[2, 4], - msg="Array expression with field refs resolved", + msg="$map array expression with field refs resolved", ), ] -# $let and system variables # Property [Variables]: $map works with $let and system variables. LET_AND_VARIABLE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="let_variable", + "let_variable", expression={ "$let": { "vars": {"arr": "$values"}, @@ -83,17 +82,17 @@ }, doc={"values": [1, 2, 3]}, expected=[2, 3, 4], - msg="Should work with $let variables", + msg="$map should work with $let variables", ), ExpressionTestCase( - id="root_variable", + "root_variable", expression={"$map": {"input": "$$ROOT.values", "in": "$$this"}}, doc={"_id": 1, "values": [10, 20]}, expected=[10, 20], - msg="Should work with $$ROOT", + msg="$map should work with $$ROOT", ), ExpressionTestCase( - id="current_variable", + "current_variable", expression={"$map": {"input": "$$CURRENT.values", "in": "$$this"}}, doc={"_id": 2, "values": [10, 20]}, expected=[10, 20], @@ -101,44 +100,43 @@ ), ] -# Null/missing via expression +# Property [Null/Missing Fields]: null and missing field paths produce null results. # Property [Null/Missing Fields]: $map handles null and missing field paths. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="missing_field", + "missing_field", expression={"$map": {"input": "$nonexistent", "in": "$$this"}}, doc={"other": 1}, expected=None, - msg="Missing field should return null", + msg="$map missing field should return null", ), ExpressionTestCase( - id="missing_input_type_is_null", + "missing_input_type_is_null", expression={"$type": {"$map": {"input": "$nonexistent", "in": "$$this"}}}, doc={"x": 1}, expected="null", - msg="Missing field should produce null type", + msg="$map missing field should produce null type", ), ExpressionTestCase( - id="remove_variable", + "remove_variable", expression={"$map": {"input": "$$REMOVE", "in": "$$this"}}, doc={"x": 1}, expected=None, msg="$$REMOVE propagates null", ), ExpressionTestCase( - id="missing_field_in_expression", + "missing_field_in_expression", expression={"$map": {"input": "$arr", "in": "$missing"}}, doc={"arr": [1, 2, 3]}, expected=[None, None, None], - msg="Missing field in 'in' should produce null for each element", + msg="$map missing field in 'in' should produce null for each element", ), ] -# Nested $map # Property [Nested Map]: $map can be nested inside another $map. NESTED_MAP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_map", + "nested_map", expression={ "$map": { "input": "$arr", @@ -154,16 +152,16 @@ }, doc={"arr": [[1, 2], [3, 4]]}, expected=[[2, 4], [6, 8]], - msg="Nested $map should process 2D array", + msg="$map nested $map should process 2D array", ), ] -# $map within $reduce and vice versa +# Property [Reduce Interaction]: $map composes correctly with $reduce scoping. # Property [Reduce Interaction]: $map output works with $reduce. REDUCE_INTERACTION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="map_within_reduce", + "map_within_reduce", expression={ "$reduce": { "input": [4, 5, 6], @@ -178,7 +176,7 @@ msg="$map's $$this should reference $map elements, not $reduce's", ), ExpressionTestCase( - id="reduce_within_map", + "reduce_within_map", expression={ "$map": { "input": [100, 50], @@ -197,25 +195,25 @@ ), ] -# $$ROOT returning full document, $$REMOVE behavior +# Property [System Variables]: $map works with $$ROOT and $$REMOVE. # Property [System Variables]: $map works with $$ROOT and $$CURRENT. SYSTEM_VAR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="root_returns_full_doc", + "root_returns_full_doc", expression={"$map": {"input": "$arr", "in": "$$ROOT"}}, doc={"_id": 1, "arr": [1, 2]}, expected=[{"_id": 1, "arr": [1, 2]}, {"_id": 1, "arr": [1, 2]}], msg="$$ROOT should return root document for each element", ), ExpressionTestCase( - id="root_field_access", + "root_field_access", expression={"$map": {"input": "$arr", "in": "$$ROOT.name"}}, doc={"_id": 1, "arr": [1, 2], "name": "test"}, expected=["test", "test"], msg="$$ROOT.field should access root field for each element", ), ExpressionTestCase( - id="remove_in_cond_becomes_null", + "remove_in_cond_becomes_null", expression={ "$map": { "input": [1, 10, 2, 20], @@ -229,7 +227,6 @@ ] -# Aggregate and test ALL_EXPR_TESTS = ( FIELD_LOOKUP_TESTS + LET_AND_VARIABLE_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py index e4b7f1aa1..4a427fd5f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py @@ -22,88 +22,86 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# Error: non-object argument # Property [Object Argument]: $map rejects non-object arguments. NON_OBJECT_ARG_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="null_arg", + "null_arg", expression={"$map": None}, error_code=EXPRESSION_NON_OBJECT_ARG_ERROR, - msg="Null arg should error", + msg="$map null arg should error", ), ExpressionTestCase( - id="int_arg", + "int_arg", expression={"$map": 1}, error_code=EXPRESSION_NON_OBJECT_ARG_ERROR, - msg="Int arg should error", + msg="$map int arg should error", ), ExpressionTestCase( - id="string_arg", + "string_arg", expression={"$map": "string"}, error_code=EXPRESSION_NON_OBJECT_ARG_ERROR, - msg="String arg should error", + msg="$map string arg should error", ), ExpressionTestCase( - id="array_arg", + "array_arg", expression={"$map": [1, 2]}, error_code=EXPRESSION_NON_OBJECT_ARG_ERROR, - msg="Array arg should error", + msg="$map array arg should error", ), ExpressionTestCase( - id="bool_arg", + "bool_arg", expression={"$map": True}, error_code=EXPRESSION_NON_OBJECT_ARG_ERROR, - msg="Bool arg should error", + msg="$map bool arg should error", ), ] -# Error: unknown fields +# Property [Unknown Fields]: $map rejects unrecognized fields in the argument object. # Property [Unknown Fields]: $map rejects unknown fields in the argument. UNKNOWN_FIELD_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="extra_unknown", + "extra_unknown", expression={"$map": {"input": [1], "in": "$$this", "unknown": 1}}, error_code=MAP_UNKNOWN_FIELD_ERROR, - msg="Extra unknown field should error", + msg="$map extra unknown field should error", ), ExpressionTestCase( - id="misspelled_inputs", + "misspelled_inputs", expression={"$map": {"inputs": [1], "in": "$$this"}}, error_code=MAP_UNKNOWN_FIELD_ERROR, - msg="Misspelled 'inputs' should error", + msg="$map misspelled 'inputs' should error", ), ] -# Error: missing required fields +# Property [Required Fields]: $map rejects when required fields are missing. # Property [Required Fields]: $map requires the input and in fields. MISSING_REQUIRED_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="missing_input", + "missing_input", expression={"$map": {"in": "$$this"}}, error_code=MAP_MISSING_INPUT_ERROR, - msg="Missing input should error", + msg="$map missing input should error", ), ExpressionTestCase( - id="missing_in", + "missing_in", expression={"$map": {"input": [1, 2, 3]}}, error_code=MAP_MISSING_IN_ERROR, - msg="Missing in should error", + msg="$map missing in should error", ), ExpressionTestCase( - id="missing_in_with_as", + "missing_in_with_as", expression={"$map": {"input": [1, 2, 3], "as": "x"}}, error_code=MAP_MISSING_IN_ERROR, - msg="Missing in with as should error", + msg="$map missing in with as should error", ), ExpressionTestCase( - id="empty_object", + "empty_object", expression={"$map": {}}, error_code=MAP_MISSING_INPUT_ERROR, - msg="Empty object should error", + msg="$map empty object should error", ), ] -# 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/range/test_range_boundary.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py index 33f94a23d..32fe0916a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py @@ -17,45 +17,45 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import INT32_MAX, INT32_MAX_MINUS_1, INT32_MIN -# Success: INT32 boundary values +# Property [INT32 Boundaries]: $range works at INT32 boundary values. INT32_BOUNDARY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int32_max_eq", + "int32_max_eq", doc={"start": INT32_MAX, "end": INT32_MAX}, expression={"$range": ["$start", "$end"]}, expected=[], - msg="INT32_MAX == INT32_MAX should be empty", + msg="$range iNT32_MAX == INT32_MAX should be empty", ), ExpressionTestCase( - id="int32_min_eq", + "int32_min_eq", doc={"start": INT32_MIN, "end": INT32_MIN}, expression={"$range": ["$start", "$end"]}, expected=[], - msg="INT32_MIN == INT32_MIN should be empty", + msg="$range iNT32_MIN == INT32_MIN should be empty", ), ExpressionTestCase( - id="int32_max_minus1", + "int32_max_minus1", doc={"start": INT32_MAX_MINUS_1, "end": INT32_MAX}, expression={"$range": ["$start", "$end"]}, expected=[INT32_MAX_MINUS_1], - msg="INT32_MAX-1 to INT32_MAX should produce single element", + msg="$range iNT32_MAX-1 to INT32_MAX should produce single element", ), ExpressionTestCase( - id="int32_min_to_plus3", + "int32_min_to_plus3", doc={"start": INT32_MIN, "end": INT32_MIN + 3}, expression={"$range": ["$start", "$end"]}, expected=[INT32_MIN, INT32_MIN + 1, INT32_MIN + 2], - msg="INT32_MIN to INT32_MIN+3", + msg="$range iNT32_MIN to INT32_MIN+3", ), ExpressionTestCase( - id="step_int32_max", + "step_int32_max", doc={"start": 0, "end": INT32_MAX, "step": INT32_MAX}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0], - msg="Step INT32_MAX should produce single element", + msg="$range step INT32_MAX should produce single element", ), ExpressionTestCase( - id="near_int32_max", + "near_int32_max", doc={"start": INT32_MAX - 7, "end": INT32_MAX}, expression={"$range": ["$start", "$end"]}, expected=[ @@ -67,18 +67,17 @@ INT32_MAX - 2, INT32_MAX - 1, ], - msg="Near INT32_MAX range", + msg="$range near INT32_MAX range", ), ExpressionTestCase( - id="cross_int32_boundary", + "cross_int32_boundary", doc={"start": INT32_MIN + 1, "end": INT32_MAX, "step": INT32_MAX}, expression={"$range": ["$start", "$end", "$step"]}, expected=[INT32_MIN + 1, 0], - msg="Cross INT32 boundary with large step", + msg="$range cross INT32 boundary with large step", ), ] -# Aggregate and test ALL_BOUNDARY_TESTS = INT32_BOUNDARY_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py index d3736d6a9..7720fac92 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py @@ -16,202 +16,202 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# Success: basic ascending ranges (default step=1) +# Property [Basic Ascending]: $range generates ascending integer sequences. BASIC_ASC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="zero_to_five", + "zero_to_five", doc={"start": 0, "end": 5}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2, 3, 4], - msg="Should generate 0..4", + msg="$range should generate 0..4", ), ExpressionTestCase( - id="one_to_four", + "one_to_four", doc={"start": 1, "end": 4}, expression={"$range": ["$start", "$end"]}, expected=[1, 2, 3], - msg="Should generate 1..3", + msg="$range should generate 1..3", ), ExpressionTestCase( - id="negative_range", + "negative_range", doc={"start": -5, "end": -1}, expression={"$range": ["$start", "$end"]}, expected=[-5, -4, -3, -2], - msg="Should generate -5..-2", + msg="$range should generate -5..-2", ), ExpressionTestCase( - id="start_equals_end", + "start_equals_end", doc={"start": 5, "end": 5}, expression={"$range": ["$start", "$end"]}, expected=[], - msg="Should return empty when start equals end", + msg="$range should return empty when start equals end", ), ExpressionTestCase( - id="start_greater_than_end", + "start_greater_than_end", doc={"start": 10, "end": 5}, expression={"$range": ["$start", "$end"]}, expected=[], - msg="Should return empty when start > end with default step", + msg="$range should return empty when start > end with default step", ), ] -# Success: custom step +# Property [Custom Step]: $range respects custom step values. # Property [Step]: $range respects custom step values. STEP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="step_two", + "step_two", doc={"start": 0, "end": 10, "step": 2}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 2, 4, 6, 8], - msg="Should generate with step 2", + msg="$range should generate with step 2", ), ExpressionTestCase( - id="step_three", + "step_three", doc={"start": 0, "end": 10, "step": 3}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 3, 6, 9], - msg="Should generate with step 3", + msg="$range should generate with step 3", ), ExpressionTestCase( - id="step_five", + "step_five", doc={"start": 0, "end": 20, "step": 5}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 5, 10, 15], - msg="Should generate with step 5", + msg="$range should generate with step 5", ), ExpressionTestCase( - id="step_one_explicit", + "step_one_explicit", doc={"start": 0, "end": 3, "step": 1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 1, 2], - msg="Explicit step 1 same as default", + msg="$range explicit step 1 same as default", ), ExpressionTestCase( - id="step_overshoots", + "step_overshoots", doc={"start": 0, "end": 5, "step": 3}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 3], - msg="Should stop when step overshoots end", + msg="$range should stop when step overshoots end", ), ExpressionTestCase( - id="step_exactly_reaches", + "step_exactly_reaches", doc={"start": 0, "end": 6, "step": 2}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 2, 4], - msg="End is exclusive even when step exactly reaches it", + msg="$range end is exclusive even when step exactly reaches it", ), ExpressionTestCase( - id="start_nonzero", + "start_nonzero", doc={"start": 5, "end": 15}, expression={"$range": ["$start", "$end"]}, expected=[5, 6, 7, 8, 9, 10, 11, 12, 13, 14], - msg="Should work with nonzero start", + msg="$range should work with nonzero start", ), ExpressionTestCase( - id="step_4", + "step_4", doc={"start": 5, "end": 15, "step": 4}, expression={"$range": ["$start", "$end", "$step"]}, expected=[5, 9, 13], - msg="Should work with step 4", + msg="$range should work with step 4", ), ] -# Success: negative step (descending) +# Property [Negative Step]: $range generates descending sequences with negative step. NEGATIVE_STEP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="descending_basic", + "descending_basic", doc={"start": 5, "end": 0, "step": -1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[5, 4, 3, 2, 1], - msg="Should generate descending 5..1", + msg="$range should generate descending 5..1", ), ExpressionTestCase( - id="descending_step_two", + "descending_step_two", doc={"start": 10, "end": 0, "step": -2}, expression={"$range": ["$start", "$end", "$step"]}, expected=[10, 8, 6, 4, 2], - msg="Should generate descending with step -2", + msg="$range should generate descending with step -2", ), ExpressionTestCase( - id="descending_negative_range", + "descending_negative_range", doc={"start": -1, "end": -5, "step": -1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[-1, -2, -3, -4], - msg="Should generate descending in negative range", + msg="$range should generate descending in negative range", ), ExpressionTestCase( - id="descending_start_equals_end", + "descending_start_equals_end", doc={"start": 5, "end": 5, "step": -1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[], - msg="Should return empty when start equals end with negative step", + msg="$range should return empty when start equals end with negative step", ), ExpressionTestCase( - id="descending_wrong_direction", + "descending_wrong_direction", doc={"start": 0, "end": 5, "step": -1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[], - msg="Should return empty when step direction mismatches", + msg="$range should return empty when step direction mismatches", ), ExpressionTestCase( - id="descending_step_neg3", + "descending_step_neg3", doc={"start": 10, "end": 0, "step": -3}, expression={"$range": ["$start", "$end", "$step"]}, expected=[10, 7, 4, 1], - msg="Should generate descending with step -3", + msg="$range should generate descending with step -3", ), ExpressionTestCase( - id="descending_past_zero", + "descending_past_zero", doc={"start": 5, "end": -1, "step": -1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[5, 4, 3, 2, 1, 0], - msg="Should descend past zero", + msg="$range should descend past zero", ), ] -# Success: empty results +# Property [Empty Result]: $range returns empty when start/end/step produce no elements. EMPTY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="ascending_wrong_direction", + "ascending_wrong_direction", doc={"start": 5, "end": 0, "step": 1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[], - msg="Should return empty when ascending step but start > end", + msg="$range should return empty when ascending step but start > end", ), ExpressionTestCase( - id="zero_zero_pos_step", + "zero_zero_pos_step", doc={"start": 0, "end": 0, "step": 1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[], - msg="0 to 0 step 1 should be empty", + msg="$range 0 to 0 step 1 should be empty", ), ExpressionTestCase( - id="zero_zero_neg_step", + "zero_zero_neg_step", doc={"start": 0, "end": 0, "step": -1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[], - msg="0 to 0 step -1 should be empty", + msg="$range 0 to 0 step -1 should be empty", ), ExpressionTestCase( - id="neg_equal", + "neg_equal", doc={"start": -1, "end": -1}, expression={"$range": ["$start", "$end"]}, expected=[], - msg="-1 to -1 should be empty", + msg="$range -1 to -1 should be empty", ), ExpressionTestCase( - id="large_equal", + "large_equal", doc={"start": 1000000, "end": 1000000}, expression={"$range": ["$start", "$end"]}, expected=[], - msg="Large equal start/end should be empty", + msg="$range large equal start/end should be empty", ), ExpressionTestCase( - id="neg_mismatch", + "neg_mismatch", doc={"start": -1, "end": -5, "step": 1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[], - msg="Negative range with positive step should be empty", + msg="$range negative range with positive step should be empty", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py index 2675108bb..f23d961e6 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py @@ -21,36 +21,36 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# Field path lookups +# Property [Field Path Resolution]: $map resolves nested and composite field paths. # Property [Field Lookup]: $range resolves field paths in expressions. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_field_path", + "nested_field_path", expression={"$range": ["$a.start", "$a.end"]}, doc={"a": {"start": 0, "end": 3}}, expected=[0, 1, 2], - msg="Should resolve nested field paths", + msg="$range should resolve nested field paths", ), ExpressionTestCase( - id="deeply_nested_field", + "deeply_nested_field", expression={"$range": ["$a.b.start", "$a.b.end"]}, doc={"a": {"b": {"start": 1, "end": 4}}}, expected=[1, 2, 3], - msg="Should resolve deeply nested field paths", + msg="$range should resolve deeply nested field paths", ), ExpressionTestCase( - id="nested_expr_start_end", + "nested_expr_start_end", expression={"$range": [{"$add": [1, 2]}, {"$multiply": [2, 5]}]}, doc={"_placeholder": 1}, expected=[3, 4, 5, 6, 7, 8, 9], - msg="Should support nested expressions as start and end", + msg="$range should support nested expressions as start and end", ), ] -# $let and system variables +# Property [Variables]: $map works with $let and system variables. LET_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="let_variable", + "let_variable", expression={ "$let": { "vars": {"s": "$start", "e": "$end"}, @@ -59,24 +59,24 @@ }, doc={"start": 0, "end": 3}, expected=[0, 1, 2], - msg="Should work with $let variables", + msg="$range should work with $let variables", ), ExpressionTestCase( - id="root_variable", + "root_variable", expression={"$range": ["$$ROOT.start", "$$ROOT.end"]}, doc={"_id": 1, "start": 0, "end": 3}, expected=[0, 1, 2], - msg="Should work with $$ROOT", + msg="$range should work with $$ROOT", ), ExpressionTestCase( - id="current_variable", + "current_variable", expression={"$range": ["$$CURRENT.start", "$$CURRENT.end"]}, doc={"_id": 2, "start": 0, "end": 3}, expected=[0, 1, 2], msg="$$CURRENT should be equivalent to field path", ), ExpressionTestCase( - id="remove_variable", + "remove_variable", expression={"$range": ["$$REMOVE", 5]}, doc={"x": 1}, error_code=RANGE_START_NOT_INT32_ERROR, @@ -84,68 +84,66 @@ ), ] -# Null/missing via expression — $range does NOT propagate null, it errors # Property [Null/Missing Fields]: $range errors on null and missing field paths. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="missing_start_field", + "missing_start_field", expression={"$range": ["$nonexistent", 5]}, doc={"other": 1}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Missing start field should error", + msg="$range missing start field should error", ), ExpressionTestCase( - id="missing_end_field", + "missing_end_field", expression={"$range": [0, "$nonexistent"]}, doc={"other": 1}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Missing end field should error", + msg="$range missing end field should error", ), ExpressionTestCase( - id="null_start_field", + "null_start_field", expression={"$range": ["$a", 5]}, doc={"a": None}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Null start field should error", + msg="$range null start field should error", ), ExpressionTestCase( - id="null_end_field", + "null_end_field", expression={"$range": [0, "$a"]}, doc={"a": None}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Null end field should error", + msg="$range null end field should error", ), ExpressionTestCase( - id="null_step_field", + "null_step_field", expression={"$range": [0, 5, "$a"]}, doc={"a": None}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Null step field should error", + msg="$range null step field should error", ), ExpressionTestCase( - id="all_missing_fields", + "all_missing_fields", expression={"$range": ["$a", "$b"]}, doc={"_placeholder": 1}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="All missing should error on start first", + msg="$range all missing should error on start first", ), ExpressionTestCase( - id="composite_array_path_error", + "composite_array_path_error", expression={"$range": ["$a.b", 5]}, doc={"a": [{"b": 0}, {"b": 5}]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Composite array path should error", + msg="$range composite array path should error", ), ExpressionTestCase( - id="array_index_path_error", + "array_index_path_error", expression={"$range": ["$a.0", "$a.1", "$a.2"]}, doc={"a": [0, 5, 2]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Array index path should error in expression context", + msg="$range array index path should error in expression context", ), ] -# Aggregate and test ALL_EXPR_TESTS = FIELD_LOOKUP_TESTS + LET_TESTS + NULL_MISSING_EXPR_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py index fdf849c9f..1a52f5c1e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py @@ -17,192 +17,192 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# Success: numeric type acceptance (int64, whole doubles, whole decimal128) +# Property [Numeric Type Acceptance]: $range accepts Int64, whole doubles, and whole Decimal128. NUMERIC_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int64_args", + "int64_args", doc={"start": Int64(0), "end": Int64(3)}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], - msg="Should accept Int64 arguments", + msg="$range should accept Int64 arguments", ), ExpressionTestCase( - id="whole_double_args", + "whole_double_args", doc={"start": 0.0, "end": 3.0}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], - msg="Should accept whole-number double arguments", + msg="$range should accept whole-number double arguments", ), ExpressionTestCase( - id="whole_decimal128_args", + "whole_decimal128_args", doc={"start": Decimal128("0"), "end": Decimal128("3")}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], - msg="Should accept whole-number Decimal128 arguments", + msg="$range should accept whole-number Decimal128 arguments", ), ExpressionTestCase( - id="int64_step", + "int64_step", doc={"start": 0, "end": 6, "step": Int64(2)}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 2, 4], - msg="Should accept Int64 step", + msg="$range should accept Int64 step", ), ExpressionTestCase( - id="whole_double_step", + "whole_double_step", doc={"start": 0, "end": 6, "step": 2.0}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 2, 4], - msg="Should accept whole-number double step", + msg="$range should accept whole-number double step", ), ExpressionTestCase( - id="whole_decimal128_step", + "whole_decimal128_step", doc={"start": 0, "end": 6, "step": Decimal128("2")}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0, 2, 4], - msg="Should accept whole-number Decimal128 step", + msg="$range should accept whole-number Decimal128 step", ), ExpressionTestCase( - id="decimal128_trailing_zero_start", + "decimal128_trailing_zero_start", doc={"start": Decimal128("0.0"), "end": 3}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], - msg="Should accept Decimal128 0.0 as start", + msg="$range should accept Decimal128 0.0 as start", ), ExpressionTestCase( - id="decimal128_trailing_zero_end", + "decimal128_trailing_zero_end", doc={"start": 0, "end": Decimal128("3.0")}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], - msg="Should accept Decimal128 3.0 as end", + msg="$range should accept Decimal128 3.0 as end", ), ExpressionTestCase( - id="decimal128_trailing_zero_both", + "decimal128_trailing_zero_both", doc={"start": Decimal128("0.0"), "end": Decimal128("3.0")}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], - msg="Should accept Decimal128 0.0 start and 3.0 end", + msg="$range should accept Decimal128 0.0 start and 3.0 end", ), ] -# Success: single element result +# Property [Single Element]: $range produces single-element arrays at boundaries. # Property [Single Element]: $range produces single-element arrays. SINGLE_ELEMENT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="single_element", + "single_element", doc={"start": 0, "end": 1}, expression={"$range": ["$start", "$end"]}, expected=[0], - msg="Should return single element", + msg="$range should return single element", ), ExpressionTestCase( - id="desc_single", + "desc_single", doc={"start": 1, "end": 0, "step": -1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[1], - msg="Descending single element", + msg="$range descending single element", ), ExpressionTestCase( - id="step_eq_range", + "step_eq_range", doc={"start": 0, "end": 5, "step": 5}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0], - msg="Step equal to range should produce single element", + msg="$range step equal to range should produce single element", ), ExpressionTestCase( - id="step_gt_range", + "step_gt_range", doc={"start": 0, "end": 5, "step": 10}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0], - msg="Step greater than range should produce single element", + msg="$range step greater than range should produce single element", ), ] -# Success: negative number ranges +# Property [Negative Numbers]: $range handles negative start, end, and crossing zero. NEGATIVE_RANGE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="neg_to_zero", + "neg_to_zero", doc={"start": -5, "end": 0}, expression={"$range": ["$start", "$end"]}, expected=[-5, -4, -3, -2, -1], - msg="Negative start to zero ascending", + msg="$range negative start to zero ascending", ), ExpressionTestCase( - id="zero_to_neg", + "zero_to_neg", doc={"start": 0, "end": -5, "step": -1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[0, -1, -2, -3, -4], - msg="Zero to negative descending", + msg="$range zero to negative descending", ), ExpressionTestCase( - id="cross_zero_asc", + "cross_zero_asc", doc={"start": -3, "end": 3}, expression={"$range": ["$start", "$end"]}, expected=[-3, -2, -1, 0, 1, 2], - msg="Crossing zero ascending", + msg="$range crossing zero ascending", ), ExpressionTestCase( - id="cross_zero_desc", + "cross_zero_desc", doc={"start": 3, "end": -3, "step": -1}, expression={"$range": ["$start", "$end", "$step"]}, expected=[3, 2, 1, 0, -1, -2], - msg="Crossing zero descending", + msg="$range crossing zero descending", ), ExpressionTestCase( - id="neg_desc_step2", + "neg_desc_step2", doc={"start": -10, "end": -20, "step": -2}, expression={"$range": ["$start", "$end", "$step"]}, expected=[-10, -12, -14, -16, -18], - msg="Negative descending with step -2", + msg="$range negative descending with step -2", ), ExpressionTestCase( - id="neg_asc_step2", + "neg_asc_step2", doc={"start": -10, "end": -1, "step": 2}, expression={"$range": ["$start", "$end", "$step"]}, expected=[-10, -8, -6, -4, -2], - msg="Negative ascending with step 2", + msg="$range negative ascending with step 2", ), ] -# Success: large range +# Property [Large Range]: $range generates large sequences. LARGE_RANGE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="large_range", + "large_range", doc={"start": 0, "end": 1000}, expression={"$range": ["$start", "$end"]}, expected=list(range(1000)), - msg="Should generate large range", + msg="$range should generate large range", ), ] -# Success: negative zero start/end +# Property [Negative Zero]: $range treats negative zero as zero. NEG_ZERO_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="neg_zero_double_start", + "neg_zero_double_start", doc={"start": -0.0, "end": 5}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2, 3, 4], - msg="Negative zero double start treated as 0", + msg="$range negative zero double start treated as 0", ), ExpressionTestCase( - id="neg_zero_decimal_start", + "neg_zero_decimal_start", doc={"start": Decimal128("-0"), "end": 5}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2, 3, 4], - msg="Negative zero Decimal128 start treated as 0", + msg="$range negative zero Decimal128 start treated as 0", ), ExpressionTestCase( - id="neg_zero_double_end", + "neg_zero_double_end", doc={"start": 0, "end": -0.0}, expression={"$range": ["$start", "$end"]}, expected=[], - msg="Negative zero double end treated as 0", + msg="$range negative zero double end treated as 0", ), ExpressionTestCase( - id="neg_zero_decimal_end", + "neg_zero_decimal_end", doc={"start": 0, "end": Decimal128("-0")}, expression={"$range": ["$start", "$end"]}, expected=[], - msg="Negative zero Decimal128 end treated as 0", + msg="$range negative zero Decimal128 end treated as 0", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_type_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_type_errors.py index 43db6c022..34474fda6 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_type_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_type_errors.py @@ -23,260 +23,260 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# Error: non-numeric start +# Property [Non-Numeric Start]: $range rejects non-numeric types for start. NON_NUMERIC_START_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="string_start", + "string_start", doc={"start": "hello", "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject string start", + msg="$range should reject string start", ), ExpressionTestCase( - id="bool_start", + "bool_start", doc={"start": True, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject bool start", + msg="$range should reject bool start", ), ExpressionTestCase( - id="null_start", + "null_start", doc={"start": None, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject null start", + msg="$range should reject null start", ), ExpressionTestCase( - id="object_start", + "object_start", doc={"start": {"a": 1}, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject object start", + msg="$range should reject object start", ), ExpressionTestCase( - id="array_start", + "array_start", doc={"start": [1], "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject array start", + msg="$range should reject array start", ), ExpressionTestCase( - id="objectid_start", + "objectid_start", doc={"start": ObjectId(), "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject objectid start", + msg="$range should reject objectid start", ), ExpressionTestCase( - id="datetime_start", + "datetime_start", doc={"start": datetime(2024, 1, 1, tzinfo=timezone.utc), "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject datetime start", + msg="$range should reject datetime start", ), ExpressionTestCase( - id="binary_start", + "binary_start", doc={"start": Binary(b"x", 0), "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject binary start", + msg="$range should reject binary start", ), ExpressionTestCase( - id="regex_start", + "regex_start", doc={"start": Regex("x"), "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject regex start", + msg="$range should reject regex start", ), ExpressionTestCase( - id="maxkey_start", + "maxkey_start", doc={"start": MaxKey(), "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject maxkey start", + msg="$range should reject maxkey start", ), ExpressionTestCase( - id="minkey_start", + "minkey_start", doc={"start": MinKey(), "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject minkey start", + msg="$range should reject minkey start", ), ExpressionTestCase( - id="timestamp_start", + "timestamp_start", doc={"start": Timestamp(0, 0), "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INT32_ERROR, - msg="Should reject timestamp start", + msg="$range should reject timestamp start", ), ] -# Error: non-numeric end +# Property [Non-Numeric End]: $range rejects non-numeric types for end. NON_NUMERIC_END_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="string_end", + "string_end", doc={"start": 0, "end": "hello"}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject string end", + msg="$range should reject string end", ), ExpressionTestCase( - id="bool_end", + "bool_end", doc={"start": 0, "end": True}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject bool end", + msg="$range should reject bool end", ), ExpressionTestCase( - id="null_end", + "null_end", doc={"start": 0, "end": None}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject null end", + msg="$range should reject null end", ), ExpressionTestCase( - id="object_end", + "object_end", doc={"start": 0, "end": {"a": 1}}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject object end", + msg="$range should reject object end", ), ExpressionTestCase( - id="array_end", + "array_end", doc={"start": 0, "end": [1]}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject array end", + msg="$range should reject array end", ), ExpressionTestCase( - id="objectid_end", + "objectid_end", doc={"start": 0, "end": ObjectId()}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject objectid end", + msg="$range should reject objectid end", ), ExpressionTestCase( - id="datetime_end", + "datetime_end", doc={"start": 0, "end": datetime(2024, 1, 1, tzinfo=timezone.utc)}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject datetime end", + msg="$range should reject datetime end", ), ExpressionTestCase( - id="binary_end", + "binary_end", doc={"start": 0, "end": Binary(b"x", 0)}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject binary end", + msg="$range should reject binary end", ), ExpressionTestCase( - id="regex_end", + "regex_end", doc={"start": 0, "end": Regex("x")}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject regex end", + msg="$range should reject regex end", ), ExpressionTestCase( - id="maxkey_end", + "maxkey_end", doc={"start": 0, "end": MaxKey()}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject maxkey end", + msg="$range should reject maxkey end", ), ExpressionTestCase( - id="minkey_end", + "minkey_end", doc={"start": 0, "end": MinKey()}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject minkey end", + msg="$range should reject minkey end", ), ExpressionTestCase( - id="timestamp_end", + "timestamp_end", doc={"start": 0, "end": Timestamp(0, 0)}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_NUMERIC_ERROR, - msg="Should reject timestamp end", + msg="$range should reject timestamp end", ), ] -# Error: non-numeric step +# Property [Non-Numeric Step]: $range rejects non-numeric types for step. NON_NUMERIC_STEP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="string_step", + "string_step", doc={"start": 0, "end": 5, "step": "bad"}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject string step", + msg="$range should reject string step", ), ExpressionTestCase( - id="bool_step", + "bool_step", doc={"start": 0, "end": 5, "step": True}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject bool step", + msg="$range should reject bool step", ), ExpressionTestCase( - id="object_step", + "object_step", doc={"start": 0, "end": 5, "step": {"a": 1}}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject object step", + msg="$range should reject object step", ), ExpressionTestCase( - id="array_step", + "array_step", doc={"start": 0, "end": 5, "step": [1]}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject array step", + msg="$range should reject array step", ), ExpressionTestCase( - id="objectid_step", + "objectid_step", doc={"start": 0, "end": 5, "step": ObjectId()}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject objectid step", + msg="$range should reject objectid step", ), ExpressionTestCase( - id="datetime_step", + "datetime_step", doc={"start": 0, "end": 5, "step": datetime(2024, 1, 1, tzinfo=timezone.utc)}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject datetime step", + msg="$range should reject datetime step", ), ExpressionTestCase( - id="binary_step", + "binary_step", doc={"start": 0, "end": 5, "step": Binary(b"x", 0)}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject binary step", + msg="$range should reject binary step", ), ExpressionTestCase( - id="regex_step", + "regex_step", doc={"start": 0, "end": 5, "step": Regex("x")}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject regex step", + msg="$range should reject regex step", ), ExpressionTestCase( - id="maxkey_step", + "maxkey_step", doc={"start": 0, "end": 5, "step": MaxKey()}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject maxkey step", + msg="$range should reject maxkey step", ), ExpressionTestCase( - id="minkey_step", + "minkey_step", doc={"start": 0, "end": 5, "step": MinKey()}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject minkey step", + msg="$range should reject minkey step", ), ExpressionTestCase( - id="timestamp_step", + "timestamp_step", doc={"start": 0, "end": 5, "step": Timestamp(0, 0)}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_NUMERIC_ERROR, - msg="Should reject timestamp step", + msg="$range should reject timestamp step", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py index 92fd48c4e..f0704fa1b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py @@ -37,317 +37,316 @@ INT64_MIN, ) -# Error: non-integral start +# Property [Non-Integral Start]: $range rejects fractional numeric values for start. NON_INTEGRAL_START_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="fractional_start", + "fractional_start", doc={"start": 1.5, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject fractional start", + msg="$range should reject fractional start", ), ExpressionTestCase( - id="decimal128_fractional_start", + "decimal128_fractional_start", doc={"start": Decimal128("0.5"), "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject fractional Decimal128 start", + msg="$range should reject fractional Decimal128 start", ), ExpressionTestCase( - id="negative_fractional_start", + "negative_fractional_start", doc={"start": -1.5, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject negative fractional start", + msg="$range should reject negative fractional start", ), ExpressionTestCase( - id="decimal128_negative_fractional_start", + "decimal128_negative_fractional_start", doc={"start": Decimal128("-1.5"), "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject negative fractional Decimal128 start", + msg="$range should reject negative fractional Decimal128 start", ), ExpressionTestCase( - id="decimal128_negative_nan_start", + "decimal128_negative_nan_start", doc={"start": Decimal128("-NaN"), "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject Decimal128 -NaN start", + msg="$range should reject Decimal128 -NaN start", ), ] -# Error: non-integral end +# Property [Non-Integral End]: $range rejects fractional numeric values for end. NON_INTEGRAL_END_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="fractional_end", + "fractional_end", doc={"start": 0, "end": 5.5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, - msg="Should reject fractional end", + msg="$range should reject fractional end", ), ExpressionTestCase( - id="decimal128_fractional_end", + "decimal128_fractional_end", doc={"start": 0, "end": Decimal128("5.5")}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, - msg="Should reject fractional Decimal128 end", + msg="$range should reject fractional Decimal128 end", ), ExpressionTestCase( - id="negative_fractional_end", + "negative_fractional_end", doc={"start": 0, "end": -1.5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, - msg="Should reject negative fractional end", + msg="$range should reject negative fractional end", ), ExpressionTestCase( - id="decimal128_negative_fractional_end", + "decimal128_negative_fractional_end", doc={"start": 0, "end": Decimal128("-1.5")}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, - msg="Should reject negative fractional Decimal128 end", + msg="$range should reject negative fractional Decimal128 end", ), ] -# Error: non-integral step +# Property [Non-Integral Step]: $range rejects fractional numeric values for step. NON_INTEGRAL_STEP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="fractional_step", + "fractional_step", doc={"start": 0, "end": 10, "step": 1.5}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, - msg="Should reject fractional step", + msg="$range should reject fractional step", ), ExpressionTestCase( - id="decimal128_fractional_step", + "decimal128_fractional_step", doc={"start": 0, "end": 10, "step": Decimal128("1.5")}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, - msg="Should reject fractional Decimal128 step", + msg="$range should reject fractional Decimal128 step", ), ExpressionTestCase( - id="negative_fractional_step", + "negative_fractional_step", doc={"start": 10, "end": 0, "step": -1.5}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, - msg="Should reject negative fractional step", + msg="$range should reject negative fractional step", ), ExpressionTestCase( - id="decimal128_negative_fractional_step", + "decimal128_negative_fractional_step", doc={"start": 10, "end": 0, "step": Decimal128("-1.5")}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, - msg="Should reject negative fractional Decimal128 step", + msg="$range should reject negative fractional Decimal128 step", ), ] -# Error: special numeric values +# Property [Special Numeric Values]: $range rejects NaN and Infinity values. # Property [Special Numerics]: $range rejects NaN and Infinity values. SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nan_start", + "nan_start", doc={"start": FLOAT_NAN, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject NaN start", + msg="$range should reject NaN start", ), ExpressionTestCase( - id="inf_start", + "inf_start", doc={"start": FLOAT_INFINITY, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject Infinity start", + msg="$range should reject Infinity start", ), ExpressionTestCase( - id="neg_inf_start", + "neg_inf_start", doc={"start": FLOAT_NEGATIVE_INFINITY, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject -Infinity start", + msg="$range should reject -Infinity start", ), ExpressionTestCase( - id="nan_end", + "nan_end", doc={"start": 0, "end": FLOAT_NAN}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, - msg="Should reject NaN end", + msg="$range should reject NaN end", ), ExpressionTestCase( - id="inf_end", + "inf_end", doc={"start": 0, "end": FLOAT_INFINITY}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, - msg="Should reject Infinity end", + msg="$range should reject Infinity end", ), ExpressionTestCase( - id="decimal128_nan_start", + "decimal128_nan_start", doc={"start": DECIMAL128_NAN, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject Decimal128 NaN start", + msg="$range should reject Decimal128 NaN start", ), ExpressionTestCase( - id="decimal128_inf_start", + "decimal128_inf_start", doc={"start": DECIMAL128_INFINITY, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject Decimal128 Infinity start", + msg="$range should reject Decimal128 Infinity start", ), ExpressionTestCase( - id="decimal128_neg_inf_end", + "decimal128_neg_inf_end", doc={"start": 0, "end": DECIMAL128_NEGATIVE_INFINITY}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, - msg="Should reject Decimal128 -Infinity end", + msg="$range should reject Decimal128 -Infinity end", ), ] -# Error: step zero → 34449 # Property [Step Zero]: $range rejects zero step value. STEP_ZERO_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="step_zero_int", + "step_zero_int", doc={"start": 0, "end": 5, "step": 0}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, - msg="Should reject step 0", + msg="$range should reject step 0", ), ExpressionTestCase( - id="step_zero_int64", + "step_zero_int64", doc={"start": 0, "end": 5, "step": Int64(0)}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, - msg="Should reject Int64 step 0", + msg="$range should reject Int64 step 0", ), ExpressionTestCase( - id="step_zero_double", + "step_zero_double", doc={"start": 0, "end": 5, "step": 0.0}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, - msg="Should reject double step 0.0", + msg="$range should reject double step 0.0", ), ExpressionTestCase( - id="step_zero_decimal128", + "step_zero_decimal128", doc={"start": 0, "end": 5, "step": Decimal128("0")}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, - msg="Should reject Decimal128 step 0", + msg="$range should reject Decimal128 step 0", ), ExpressionTestCase( - id="step_neg_zero_double", + "step_neg_zero_double", doc={"start": 0, "end": 5, "step": -0.0}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, - msg="Should reject negative zero double step", + msg="$range should reject negative zero double step", ), ExpressionTestCase( - id="step_neg_zero_decimal128", + "step_neg_zero_decimal128", doc={"start": 0, "end": 5, "step": Decimal128("-0")}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, - msg="Should reject negative zero Decimal128 step", + msg="$range should reject negative zero Decimal128 step", ), ] -# Error: out of int32 range +# Property [Out of INT32 Range]: $range rejects values outside int32 range. OUT_OF_INT32_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="start_int64_max", + "start_int64_max", doc={"start": INT64_MAX, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject INT64_MAX start", + msg="$range should reject INT64_MAX start", ), ExpressionTestCase( - id="start_int64_min", + "start_int64_min", doc={"start": INT64_MIN, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject INT64_MIN start", + msg="$range should reject INT64_MIN start", ), ExpressionTestCase( - id="end_int64_max", + "end_int64_max", doc={"start": 0, "end": INT64_MAX}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, - msg="Should reject INT64_MAX end", + msg="$range should reject INT64_MAX end", ), ExpressionTestCase( - id="end_int64_min", + "end_int64_min", doc={"start": 0, "end": INT64_MIN}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, - msg="Should reject INT64_MIN end", + msg="$range should reject INT64_MIN end", ), ExpressionTestCase( - id="step_int64_max", + "step_int64_max", doc={"start": 0, "end": 5, "step": INT64_MAX}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, - msg="Should reject INT64_MAX step", + msg="$range should reject INT64_MAX step", ), ExpressionTestCase( - id="start_int32_overflow", + "start_int32_overflow", doc={"start": INT32_OVERFLOW, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject INT32_OVERFLOW start", + msg="$range should reject INT32_OVERFLOW start", ), ExpressionTestCase( - id="start_int32_underflow", + "start_int32_underflow", doc={"start": INT32_UNDERFLOW, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, - msg="Should reject INT32_UNDERFLOW start", + msg="$range should reject INT32_UNDERFLOW start", ), ExpressionTestCase( - id="end_int32_overflow", + "end_int32_overflow", doc={"start": 0, "end": INT32_OVERFLOW}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, - msg="Should reject INT32_OVERFLOW end", + msg="$range should reject INT32_OVERFLOW end", ), ExpressionTestCase( - id="end_int32_underflow", + "end_int32_underflow", doc={"start": 0, "end": INT32_UNDERFLOW}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, - msg="Should reject INT32_UNDERFLOW end", + msg="$range should reject INT32_UNDERFLOW end", ), ExpressionTestCase( - id="step_int32_overflow", + "step_int32_overflow", doc={"start": 0, "end": 5, "step": INT32_OVERFLOW}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, - msg="Should reject INT32_OVERFLOW step", + msg="$range should reject INT32_OVERFLOW step", ), ExpressionTestCase( - id="step_int32_underflow", + "step_int32_underflow", doc={"start": 0, "end": 5, "step": INT32_UNDERFLOW}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, - msg="Should reject INT32_UNDERFLOW step", + msg="$range should reject INT32_UNDERFLOW step", ), ] -# Error: wrong arity +# Property [Arity]: $range rejects wrong number of arguments. ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="zero_args", + "zero_args", expression={"$range": []}, error_code=EXPRESSION_ARITY_ERROR, - msg="Should error with zero arguments", + msg="$range should error with zero arguments", ), ExpressionTestCase( - id="one_arg", + "one_arg", expression={"$range": [1]}, error_code=EXPRESSION_ARITY_ERROR, - msg="Should error with one argument", + msg="$range should error with one argument", ), ExpressionTestCase( - id="four_args", + "four_args", expression={"$range": [1, 2, 3, 4]}, error_code=EXPRESSION_ARITY_ERROR, - msg="Should error with four arguments", + msg="$range should error with four arguments", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py index 8356057a4..335eca07b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_argument_structure_errors.py @@ -27,99 +27,98 @@ # Property [Argument Structure]: $zip rejects malformed arguments. STRUCTURE_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="empty_object", + "empty_object", expression={"$zip": {}}, error_code=ZIP_MISSING_INPUTS_ERROR, - msg="Empty object should error", + msg="$zip empty object should error", ), ExpressionTestCase( - id="missing_inputs", + "missing_inputs", expression={"$zip": {"useLongestLength": True}}, error_code=ZIP_MISSING_INPUTS_ERROR, - msg="Missing inputs should error", + msg="$zip missing inputs should error", ), ExpressionTestCase( - id="unknown_field", + "unknown_field", expression={"$zip": {"inputs": [[1], [2]], "extra": 1}}, error_code=ZIP_UNKNOWN_FIELD_ERROR, - msg="Unknown field should error", + msg="$zip unknown field should error", ), ExpressionTestCase( - id="inputs_not_array", + "inputs_not_array", expression={"$zip": {"inputs": "bad"}}, error_code=ZIP_INPUT_ELEMENT_NOT_ARRAY_ERROR, - msg="Non-array inputs should error", + msg="$zip non-array inputs should error", ), ExpressionTestCase( - id="inputs_not_array_int", + "inputs_not_array_int", expression={"$zip": {"inputs": 1}}, error_code=ZIP_INPUT_ELEMENT_NOT_ARRAY_ERROR, - msg="Int inputs should error", + msg="$zip int inputs should error", ), ExpressionTestCase( - id="inputs_empty_array", + "inputs_empty_array", expression={"$zip": {"inputs": []}}, error_code=ZIP_MISSING_INPUTS_ERROR, - msg="Empty inputs array should error", + msg="$zip empty inputs array should error", ), ExpressionTestCase( - id="inputs_as_object", + "inputs_as_object", expression={"$zip": {"inputs": {"a": "b"}}}, error_code=ZIP_INPUT_ELEMENT_NOT_ARRAY_ERROR, - msg="inputs as object should error", + msg="$zip inputs as object should error", ), ] -# Non-object argument (error 34460) # Property [Object Argument]: $zip rejects non-object arguments. NON_OBJECT_ARG_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int_arg", + "int_arg", expression={"$zip": 1}, error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, - msg="Int argument should error", + msg="$zip int argument should error", ), ExpressionTestCase( - id="string_arg", + "string_arg", expression={"$zip": "abc"}, error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, - msg="String argument should error", + msg="$zip string argument should error", ), ExpressionTestCase( - id="array_arg", + "array_arg", expression={"$zip": [1, 2]}, error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, - msg="Array argument should error", + msg="$zip array argument should error", ), ExpressionTestCase( - id="null_arg", + "null_arg", expression={"$zip": None}, error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, - msg="Null argument should error", + msg="$zip null argument should error", ), ExpressionTestCase( - id="bool_arg", + "bool_arg", expression={"$zip": True}, error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, - msg="Bool argument should error", + msg="$zip bool argument should error", ), ExpressionTestCase( - id="double_arg", + "double_arg", expression={"$zip": 1.5}, error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, - msg="Double argument should error", + msg="$zip double argument should error", ), ExpressionTestCase( - id="minkey_arg", + "minkey_arg", expression={"$zip": MinKey()}, error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, - msg="MinKey argument should error", + msg="$zip minKey argument should error", ), ExpressionTestCase( - id="maxkey_arg", + "maxkey_arg", expression={"$zip": MaxKey()}, error_code=ZIP_INPUTS_NOT_ARRAY_ERROR, - msg="MaxKey argument should error", + msg="$zip maxKey argument should error", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py index a21cff594..252a45910 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py @@ -32,25 +32,25 @@ INT64_MIN, ) -# BSON types preserved after zipping +# Property [Type Preservation]: $zip preserves each element BSON type. # Property [Type Preservation]: $zip preserves each element's BSON type. BSON_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int64_values", + "int64_values", doc={"arr0": [Int64(1)], "arr1": [Int64(2)]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[Int64(1), Int64(2)]], - msg="Should preserve Int64 values", + msg="$zip should preserve Int64 values", ), ExpressionTestCase( - id="decimal128_values", + "decimal128_values", doc={"arr0": [Decimal128("1.5")], "arr1": [Decimal128("2.5")]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[Decimal128("1.5"), Decimal128("2.5")]], - msg="Should preserve Decimal128 values", + msg="$zip should preserve Decimal128 values", ), ExpressionTestCase( - id="datetime_values", + "datetime_values", doc={ "arr0": [datetime(2024, 1, 1, tzinfo=timezone.utc)], "arr1": [datetime(2024, 6, 1, tzinfo=timezone.utc)], @@ -59,48 +59,48 @@ expected=[ [datetime(2024, 1, 1, tzinfo=timezone.utc), datetime(2024, 6, 1, tzinfo=timezone.utc)] ], - msg="Should preserve datetime values", + msg="$zip should preserve datetime values", ), ExpressionTestCase( - id="objectid_values", + "objectid_values", doc={ "arr0": [ObjectId("000000000000000000000001")], "arr1": [ObjectId("000000000000000000000002")], }, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002")]], - msg="Should preserve ObjectId values", + msg="$zip should preserve ObjectId values", ), ExpressionTestCase( - id="binary_values", + "binary_values", doc={"arr0": [Binary(b"\x01", 0)], "arr1": [Binary(b"\x02", 0)]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[b"\x01", b"\x02"]], - msg="Should preserve Binary values", + msg="$zip should preserve Binary values", ), ExpressionTestCase( - id="regex_values", + "regex_values", doc={"arr0": [Regex("^a", "i")], "arr1": [Regex("^b", "i")]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[Regex("^a", "i"), Regex("^b", "i")]], - msg="Should preserve Regex values", + msg="$zip should preserve Regex values", ), ExpressionTestCase( - id="timestamp_values", + "timestamp_values", doc={"arr0": [Timestamp(1, 0)], "arr1": [Timestamp(2, 0)]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[Timestamp(1, 0), Timestamp(2, 0)]], - msg="Should preserve Timestamp values", + msg="$zip should preserve Timestamp values", ), ExpressionTestCase( - id="minkey_maxkey", + "minkey_maxkey", doc={"arr0": [MinKey()], "arr1": [MaxKey()]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[MinKey(), MaxKey()]], - msg="Should preserve MinKey/MaxKey values", + msg="$zip should preserve MinKey/MaxKey values", ), ExpressionTestCase( - id="uuid_values", + "uuid_values", doc={ "arr0": [Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))], "arr1": [Binary.from_uuid(UUID("fedcba98-7654-3210-0123-456789abcdef"))], @@ -112,66 +112,66 @@ Binary.from_uuid(UUID("fedcba98-7654-3210-0123-456789abcdef")), ] ], - msg="Should preserve UUID binary values", + msg="$zip should preserve UUID binary values", ), ] -# Mixed BSON types across arrays +# Property [Mixed Types]: $map processes arrays with mixed BSON types. across arrays # Property [Mixed Types]: $zip processes arrays with mixed BSON types. MIXED_BSON_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="mixed_bson_types", + "mixed_bson_types", doc={"arr0": [1, "two", Int64(3)], "arr1": [Decimal128("4"), True, MinKey()]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, Decimal128("4")], ["two", True], [Int64(3), MinKey()]], - msg="Should zip mixed BSON types preserving each", + msg="$zip should zip mixed BSON types preserving each", ), ] -# Special numeric values as elements +# Property [Special Numerics]: $map preserves NaN, Infinity, and boundary values. # Property [Special Numerics]: $zip preserves NaN, Infinity, and boundary values. SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="infinity_values", + "infinity_values", doc={"arr0": [FLOAT_INFINITY], "arr1": [FLOAT_NEGATIVE_INFINITY]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY]], - msg="Should preserve infinity values", + msg="$zip should preserve infinity values", ), ExpressionTestCase( - id="decimal128_infinity", + "decimal128_infinity", doc={"arr0": [DECIMAL128_INFINITY], "arr1": [DECIMAL128_NEGATIVE_INFINITY]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY]], - msg="Should preserve Decimal128 infinity values", + msg="$zip should preserve Decimal128 infinity values", ), ExpressionTestCase( - id="boundary_values", + "boundary_values", doc={"arr0": [INT32_MIN, INT32_MAX], "arr1": [INT64_MIN, INT64_MAX]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[INT32_MIN, INT64_MIN], [INT32_MAX, INT64_MAX]], - msg="Should preserve numeric boundary values", + msg="$zip should preserve numeric boundary values", ), ExpressionTestCase( - id="negative_zero", + "negative_zero", doc={"arr0": [DOUBLE_NEGATIVE_ZERO], "arr1": [0]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[DOUBLE_NEGATIVE_ZERO, 0]], - msg="Should preserve negative zero", + msg="$zip should preserve negative zero", ), ExpressionTestCase( - id="decimal128_nan", + "decimal128_nan", doc={"arr0": [DECIMAL128_NAN], "arr1": [Decimal128("1")]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[DECIMAL128_NAN, Decimal128("1")]], - msg="Should preserve Decimal128 NaN", + msg="$zip should preserve Decimal128 NaN", ), ] -# Defaults with BSON types +# Property [BSON Defaults]: $zip uses BSON-typed default values correctly. BSON_DEFAULTS_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="default_int64", + "default_int64", doc={"arr0": [1, 2, 3], "arr1": [Int64(10)]}, expression={ "$zip": { @@ -181,10 +181,10 @@ } }, expected=[[1, Int64(10)], [2, Int64(0)], [3, Int64(0)]], - msg="Should use Int64 default value", + msg="$zip should use Int64 default value", ), ExpressionTestCase( - id="default_decimal128", + "default_decimal128", doc={"arr0": [1, 2, 3], "arr1": [Decimal128("1.5")]}, expression={ "$zip": { @@ -194,10 +194,10 @@ } }, expected=[[1, Decimal128("1.5")], [2, Decimal128("0")], [3, Decimal128("0")]], - msg="Should use Decimal128 default value", + msg="$zip should use Decimal128 default value", ), ExpressionTestCase( - id="default_datetime", + "default_datetime", doc={"arr0": [1, 2], "arr1": [datetime(2024, 1, 1, tzinfo=timezone.utc)]}, expression={ "$zip": { @@ -210,10 +210,10 @@ [1, datetime(2024, 1, 1, tzinfo=timezone.utc)], [2, datetime(1970, 1, 1, tzinfo=timezone.utc)], ], - msg="Should use datetime default value", + msg="$zip should use datetime default value", ), ExpressionTestCase( - id="default_objectid", + "default_objectid", doc={"arr0": [1, 2], "arr1": [ObjectId("000000000000000000000001")]}, expression={ "$zip": { @@ -226,10 +226,10 @@ [1, ObjectId("000000000000000000000001")], [2, ObjectId("000000000000000000000000")], ], - msg="Should use ObjectId default value", + msg="$zip should use ObjectId default value", ), ExpressionTestCase( - id="default_timestamp", + "default_timestamp", doc={"arr0": [1, 2], "arr1": [Timestamp(1, 0)]}, expression={ "$zip": { @@ -239,10 +239,10 @@ } }, expected=[[1, Timestamp(1, 0)], [2, Timestamp(0, 0)]], - msg="Should use Timestamp default value", + msg="$zip should use Timestamp default value", ), ExpressionTestCase( - id="default_regex", + "default_regex", doc={"arr0": [1, 2], "arr1": [Regex("^a", "i")]}, expression={ "$zip": { @@ -252,10 +252,10 @@ } }, expected=[[1, Regex("^a", "i")], [2, Regex(".*", "")]], - msg="Should use Regex default value", + msg="$zip should use Regex default value", ), ExpressionTestCase( - id="default_minkey_maxkey", + "default_minkey_maxkey", doc={"arr0": [1, 2], "arr1": [MinKey()]}, expression={ "$zip": { @@ -265,10 +265,10 @@ } }, expected=[[1, MinKey()], [2, MaxKey()]], - msg="Should use MaxKey default value", + msg="$zip should use MaxKey default value", ), ExpressionTestCase( - id="default_binary", + "default_binary", doc={"arr0": [1, 2], "arr1": [Binary(b"\x01", 0)]}, expression={ "$zip": { @@ -278,14 +278,14 @@ } }, expected=[[1, b"\x01"], [2, b"\x00"]], - msg="Should use Binary default value", + msg="$zip should use Binary default value", ), ] -# Defaults with special numeric values +# Property [Special Numeric Defaults]: $zip uses special numeric values as defaults. SPECIAL_NUMERIC_DEFAULTS_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="default_infinity", + "default_infinity", doc={"arr0": [1, 2], "arr1": [FLOAT_INFINITY]}, expression={ "$zip": { @@ -295,10 +295,10 @@ } }, expected=[[1, FLOAT_INFINITY], [2, FLOAT_INFINITY]], - msg="Should use infinity as default", + msg="$zip should use infinity as default", ), ExpressionTestCase( - id="default_negative_infinity", + "default_negative_infinity", doc={"arr0": [1, 2], "arr1": [FLOAT_NEGATIVE_INFINITY]}, expression={ "$zip": { @@ -308,10 +308,10 @@ } }, expected=[[1, FLOAT_NEGATIVE_INFINITY], [2, FLOAT_NEGATIVE_INFINITY]], - msg="Should use negative infinity as default", + msg="$zip should use negative infinity as default", ), ExpressionTestCase( - id="default_negative_zero", + "default_negative_zero", doc={"arr0": [1, 2], "arr1": [DOUBLE_NEGATIVE_ZERO]}, expression={ "$zip": { @@ -321,10 +321,10 @@ } }, expected=[[1, DOUBLE_NEGATIVE_ZERO], [2, DOUBLE_NEGATIVE_ZERO]], - msg="Should use negative zero as default", + msg="$zip should use negative zero as default", ), ExpressionTestCase( - id="default_int32_boundaries", + "default_int32_boundaries", doc={"arr0": [1, 2], "arr1": [INT32_MIN]}, expression={ "$zip": { @@ -334,10 +334,10 @@ } }, expected=[[1, INT32_MIN], [2, INT32_MAX]], - msg="Should use INT32_MAX as default", + msg="$zip should use INT32_MAX as default", ), ExpressionTestCase( - id="default_int64_boundaries", + "default_int64_boundaries", doc={"arr0": [1, 2], "arr1": [INT64_MIN]}, expression={ "$zip": { @@ -347,10 +347,10 @@ } }, expected=[[1, INT64_MIN], [2, INT64_MAX]], - msg="Should use INT64_MAX as default", + msg="$zip should use INT64_MAX as default", ), ExpressionTestCase( - id="default_decimal128_infinity", + "default_decimal128_infinity", doc={"arr0": [1, 2], "arr1": [DECIMAL128_INFINITY]}, expression={ "$zip": { @@ -360,10 +360,10 @@ } }, expected=[[1, DECIMAL128_INFINITY], [2, DECIMAL128_NEGATIVE_INFINITY]], - msg="Should use Decimal128 negative infinity as default", + msg="$zip should use Decimal128 negative infinity as default", ), ExpressionTestCase( - id="default_decimal128_nan", + "default_decimal128_nan", doc={"arr0": [1, 2], "arr1": [DECIMAL128_NAN]}, expression={ "$zip": { @@ -373,11 +373,10 @@ } }, expected=[[1, DECIMAL128_NAN], [2, DECIMAL128_NAN]], - msg="Should use Decimal128 NaN as default", + msg="$zip should use Decimal128 NaN as default", ), ] -# Aggregate and test ALL_BSON_TESTS = ( BSON_TYPE_TESTS + MIXED_BSON_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py index dd7a13962..5d6dbc45c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py @@ -17,118 +17,117 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# Success: basic zipping — equal length arrays # Property [Basic Transform]: $zip transposes arrays element-wise. BASIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="two_int_arrays", + "two_int_arrays", doc={"arr0": [1, 2, 3], "arr1": [10, 20, 30]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 10], [2, 20], [3, 30]], - msg="Should zip two int arrays", + msg="$zip should zip two int arrays", ), ExpressionTestCase( - id="two_string_arrays", + "two_string_arrays", doc={"arr0": ["a", "b"], "arr1": ["c", "d"]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[["a", "c"], ["b", "d"]], - msg="Should zip two string arrays", + msg="$zip should zip two string arrays", ), ExpressionTestCase( - id="three_arrays", + "three_arrays", doc={"arr0": [1, 2], "arr1": [10, 20], "arr2": [100, 200]}, expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, expected=[[1, 10, 100], [2, 20, 200]], - msg="Should zip three arrays", + msg="$zip should zip three arrays", ), ExpressionTestCase( - id="mixed_type_elements", + "mixed_type_elements", doc={"arr0": [1, "two"], "arr1": [True, None]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, True], ["two", None]], - msg="Should zip arrays with mixed types", + msg="$zip should zip arrays with mixed types", ), ExpressionTestCase( - id="numeric_cross_types", + "numeric_cross_types", doc={"arr0": [1, Int64(2)], "arr1": [3.0, Decimal128("4")]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 3.0], [Int64(2), Decimal128("4")]], - msg="Should zip mixed numeric type arrays", + msg="$zip should zip mixed numeric type arrays", ), ] -# Success: unequal length — truncate to shortest (default) +# Property [Unequal Length]: $zip truncates to the shortest array by default. # Property [Unequal Length]: $zip truncates to the shortest array. UNEQUAL_LENGTH_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="first_shorter", + "first_shorter", doc={"arr0": [1, 2], "arr1": [10, 20, 30]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 10], [2, 20]], - msg="Should truncate to shorter first array", + msg="$zip should truncate to shorter first array", ), ExpressionTestCase( - id="second_shorter", + "second_shorter", doc={"arr0": [1, 2, 3], "arr1": [10, 20]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 10], [2, 20]], - msg="Should truncate to shorter second array", + msg="$zip should truncate to shorter second array", ), ExpressionTestCase( - id="one_empty", + "one_empty", doc={"arr0": [], "arr1": [1, 2, 3]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[], - msg="Empty array should produce empty result", + msg="$zip empty array should produce empty result", ), ] -# Success: useLongestLength +# Property [Longest Length]: $zip pads to longest array when useLongestLength is true. USE_LONGEST_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="longest_pads_null", + "longest_pads_null", doc={"arr0": [1, 2, 3], "arr1": [10, 20]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, expected=[[1, 10], [2, 20], [3, None]], - msg="Should pad shorter array with null", + msg="$zip should pad shorter array with null", ), ExpressionTestCase( - id="longest_both_short", + "longest_both_short", doc={"arr0": [1], "arr1": [10, 20], "arr2": [100, 200, 300]}, expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"], "useLongestLength": True}}, expected=[[1, 10, 100], [None, 20, 200], [None, None, 300]], - msg="Should pad multiple shorter arrays with null", + msg="$zip should pad multiple shorter arrays with null", ), ExpressionTestCase( - id="longest_equal_length", + "longest_equal_length", doc={"arr0": [1, 2], "arr1": [10, 20]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, expected=[[1, 10], [2, 20]], - msg="Equal length with useLongestLength should behave same", + msg="$zip equal length with useLongestLength should behave same", ), ExpressionTestCase( - id="longest_false_truncates", + "longest_false_truncates", doc={"arr0": [1, 2, 3], "arr1": [10, 20]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": False}}, expected=[[1, 10], [2, 20]], - msg="useLongestLength false should truncate", + msg="$zip useLongestLength false should truncate", ), ] -# Success: defaults +# Property [Defaults]: $zip pads shorter arrays with specified default values. # Property [Defaults]: $zip pads shorter arrays with default values. DEFAULTS_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="defaults_fill_shorter", + "defaults_fill_shorter", doc={"arr0": [1, 2, 3], "arr1": [10, 20]}, expression={ "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0, 0]} }, expected=[[1, 10], [2, 20], [3, 0]], - msg="Should fill shorter array with default value", + msg="$zip should fill shorter array with default value", ), ExpressionTestCase( - id="defaults_multiple_arrays", + "defaults_multiple_arrays", doc={"arr0": [1], "arr1": [10, 20], "arr2": [100, 200, 300]}, expression={ "$zip": { @@ -138,10 +137,10 @@ } }, expected=[[1, 10, 100], [-1, 20, 200], [-1, -2, 300]], - msg="Should fill multiple shorter arrays with respective defaults", + msg="$zip should fill multiple shorter arrays with respective defaults", ), ExpressionTestCase( - id="defaults_null_value", + "defaults_null_value", doc={"arr0": [1, 2, 3], "arr1": [10]}, expression={ "$zip": { @@ -151,10 +150,10 @@ } }, expected=[[1, 10], [2, None], [3, None]], - msg="Null defaults should work same as no defaults", + msg="$zip null defaults should work same as no defaults", ), ExpressionTestCase( - id="defaults_string", + "defaults_string", doc={"arr0": [1, 2, 3], "arr1": ["a"]}, expression={ "$zip": { @@ -164,37 +163,37 @@ } }, expected=[[1, "a"], [2, "missing"], [3, "missing"]], - msg="Should use string default value", + msg="$zip should use string default value", ), ExpressionTestCase( - id="defaults_not_used_equal_length", + "defaults_not_used_equal_length", doc={"arr0": [1, 2], "arr1": [10, 20]}, expression={ "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0, 0]} }, expected=[[1, 10], [2, 20]], - msg="Defaults not used when arrays are equal length", + msg="$zip defaults not used when arrays are equal length", ), ExpressionTestCase( - id="defaults_falsy_empty_string", + "defaults_falsy_empty_string", doc={"arr0": [1, 2], "arr1": ["a"]}, expression={ "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0, ""]} }, expected=[[1, "a"], [2, ""]], - msg="Falsy defaults (empty string) used correctly", + msg="$zip falsy defaults (empty string) used correctly", ), ExpressionTestCase( - id="defaults_false", + "defaults_false", doc={"arr0": [1, 2], "arr1": ["a"]}, expression={ "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0, False]} }, expected=[[1, "a"], [2, False]], - msg="False default used correctly", + msg="$zip false default used correctly", ), ExpressionTestCase( - id="defaults_complex_types", + "defaults_complex_types", doc={"arr0": [1, 2], "arr1": ["a"]}, expression={ "$zip": { @@ -204,10 +203,10 @@ } }, expected=[[1, "a"], [2, [1, 2]]], - msg="Complex type defaults used correctly", + msg="$zip complex type defaults used correctly", ), ExpressionTestCase( - id="defaults_nested_array", + "defaults_nested_array", doc={"arr0": [1, 2], "arr1": ["a"]}, expression={ "$zip": { @@ -217,7 +216,7 @@ } }, expected=[[1, "a"], [2, "fallback"]], - msg="Nested array default used as-is", + msg="$zip nested array default used as-is", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py index 892e521a8..ba0cf6778 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py @@ -17,176 +17,176 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# Success: empty and single element +# Property [Empty/Single]: $zip handles empty arrays and single-element arrays. DEGENERATE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="both_empty", + "both_empty", doc={"arr0": [], "arr1": []}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[], - msg="Should return empty for two empty arrays", + msg="$zip should return empty for two empty arrays", ), ExpressionTestCase( - id="three_empty", + "three_empty", doc={"arr0": [], "arr1": [], "arr2": []}, expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, expected=[], - msg="Three empty arrays return []", + msg="$zip three empty arrays return []", ), ExpressionTestCase( - id="single_empty", + "single_empty", doc={"arr0": []}, expression={"$zip": {"inputs": ["$arr0"]}}, expected=[], - msg="Single empty array returns []", + msg="$zip single empty array returns []", ), ExpressionTestCase( - id="single_element_each", + "single_element_each", doc={"arr0": [1], "arr1": [10]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 10]], - msg="Should zip single-element arrays", + msg="$zip should zip single-element arrays", ), ExpressionTestCase( - id="single_input_array", + "single_input_array", doc={"arr0": [1, 2, 3]}, expression={"$zip": {"inputs": ["$arr0"]}}, expected=[[1], [2], [3]], - msg="Single input should wrap each element", + msg="$zip single input should wrap each element", ), ExpressionTestCase( - id="all_single_element_three", + "all_single_element_three", doc={"arr0": [1], "arr1": ["a"], "arr2": [True]}, expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, expected=[[1, "a", True]], - msg="All single-element arrays produce one row", + msg="$zip all single-element arrays produce one row", ), ExpressionTestCase( - id="empty_with_longest_true", + "empty_with_longest_true", doc={"arr0": [], "arr1": [1, 2, 3]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, expected=[[None, 1], [None, 2], [None, 3]], - msg="Empty array with longest length pads with null", + msg="$zip empty array with longest length pads with null", ), ExpressionTestCase( - id="two_empty_longest_true", + "two_empty_longest_true", doc={"arr0": [], "arr1": []}, expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True}}, expected=[], - msg="Two empty arrays with longest length return []", + msg="$zip two empty arrays with longest length return []", ), ] -# Success: nested arrays as elements +# Property [Nested Arrays]: $zip preserves nested array and object elements. # Property [Nested Arrays]: $zip operates on nested array structures. NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_arrays", + "nested_arrays", doc={"arr0": [[1, 2], [3, 4]], "arr1": ["a", "b"]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[[1, 2], "a"], [[3, 4], "b"]], - msg="Should zip nested arrays as elements", + msg="$zip should zip nested arrays as elements", ), ExpressionTestCase( - id="objects_as_elements", + "objects_as_elements", doc={"arr0": [{"x": 1}, {"x": 2}], "arr1": [{"y": 10}, {"y": 20}]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[{"x": 1}, {"y": 10}], [{"x": 2}, {"y": 20}]], - msg="Objects preserved as elements", + msg="$zip objects preserved as elements", ), ExpressionTestCase( - id="mixed_types_six", + "mixed_types_six", doc={"arr0": [1, "a", None, True, {"k": 1}, [9]], "arr1": [10, 20, 30, 40, 50, 60]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, 10], ["a", 20], [None, 30], [True, 40], [{"k": 1}, 50], [[9], 60]], - msg="Mixed types preserved in transposition", + msg="$zip mixed types preserved in transposition", ), ] -# Success: null propagation +# Property [Null Propagation]: $zip returns null when any input array is null. NULL_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="null_first_input", + "null_first_input", doc={"arr0": None, "arr1": [1, 2]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=None, - msg="Should return null when first input is null", + msg="$zip should return null when first input is null", ), ExpressionTestCase( - id="null_second_input", + "null_second_input", doc={"arr0": [1, 2], "arr1": None}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=None, - msg="Should return null when second input is null", + msg="$zip should return null when second input is null", ), ExpressionTestCase( - id="all_null_inputs", + "all_null_inputs", doc={"arr0": None, "arr1": None}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=None, - msg="Should return null when all inputs are null", + msg="$zip should return null when all inputs are null", ), ExpressionTestCase( - id="null_elements_in_arrays", + "null_elements_in_arrays", doc={"arr0": [1, None], "arr1": [None, 2]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[1, None], [None, 2]], - msg="Should preserve null elements within arrays", + msg="$zip should preserve null elements within arrays", ), ] -# Success: objects as elements +# Property [Object Elements]: $zip preserves object elements within arrays. OBJECT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="arrays_of_objects", + "arrays_of_objects", doc={"arr0": [{"a": 1}], "arr1": [{"b": 2}]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[{"a": 1}, {"b": 2}]], - msg="Should zip arrays of objects", + msg="$zip should zip arrays of objects", ), ] -# Success: large arrays +# Property [Large Arrays]: $map handles arrays with many elements.s # Property [Large Arrays]: $zip handles large arrays. LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="large_arrays", + "large_arrays", doc={"arr0": list(range(1000)), "arr1": list(range(1000, 2000))}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, expected=[[i, i + 1000] for i in range(1000)], - msg="Should zip large arrays", + msg="$zip should zip large arrays", ), ] -# Success: many input arrays +# Property [Many Inputs]: $zip handles many input arrays. MANY_INPUTS_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="ten_inputs", + "ten_inputs", doc={f"arr{i}": [i] for i in range(10)}, expression={"$zip": {"inputs": [f"$arr{i}" for i in range(10)]}}, expected=[list(range(10))], - msg="Ten inputs transpose correctly", + msg="$zip ten inputs transpose correctly", ), ExpressionTestCase( - id="fifty_inputs", + "fifty_inputs", doc={f"arr{i}": [i] for i in range(50)}, expression={"$zip": {"inputs": [f"$arr{i}" for i in range(50)]}}, expected=[list(range(50))], - msg="50 single-element inputs produce one 50-element row", + msg="$zip 50 single-element inputs produce one 50-element row", ), ] -# Success: multiple arrays of different lengths +# Property [Multi-Length]: $zip handles multiple arrays of varying lengths. MULTI_LENGTH_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="three_arrays_shortest", + "three_arrays_shortest", doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, expected=[[1, 10, 100]], - msg="Three arrays shortest = 1", + msg="$zip three arrays shortest = 1", ), ExpressionTestCase( - id="three_arrays_longest_no_defaults", + "three_arrays_longest_no_defaults", doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"], "useLongestLength": True}}, expected=[ @@ -196,10 +196,10 @@ [None, None, 400], [None, None, 500], ], - msg="Three arrays longest pads with null", + msg="$zip three arrays longest pads with null", ), ExpressionTestCase( - id="three_arrays_longest_with_defaults", + "three_arrays_longest_with_defaults", doc={"arr0": [1], "arr1": [10, 20, 30], "arr2": [100, 200, 300, 400, 500]}, expression={ "$zip": { @@ -209,16 +209,16 @@ } }, expected=[[1, 10, 100], [0, 20, 200], [0, 30, 300], [0, "x", 400], [0, "x", 500]], - msg="Three arrays longest with defaults", + msg="$zip three arrays longest with defaults", ), ExpressionTestCase( - id="four_arrays_two_empty", + "four_arrays_two_empty", doc={"arr0": [], "arr1": [], "arr2": [1, 2], "arr3": [3, 4, 5]}, expression={ "$zip": {"inputs": ["$arr0", "$arr1", "$arr2", "$arr3"], "useLongestLength": True} }, expected=[[None, None, 1, 3], [None, None, 2, 4], [None, None, None, 5]], - msg="Four arrays two empty with longest", + msg="$zip four arrays two empty with longest", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py index 173086dd4..9bb6bff7c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py @@ -43,382 +43,381 @@ INT64_MIN, ) -# Error: non-array input element — standard BSON types +# Property [Non-Array Element]: $zip rejects non-array elements in inputs with all BSON types. NOT_ARRAY_ELEMENT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="string_input", + "string_input", doc={"arr0": "hello", "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject string input element", + msg="$zip should reject string input element", ), ExpressionTestCase( - id="int_input", + "int_input", doc={"arr0": 42, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject int input element", + msg="$zip should reject int input element", ), ExpressionTestCase( - id="negative_int_input", + "negative_int_input", doc={"arr0": -42, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject negative int input element", + msg="$zip should reject negative int input element", ), ExpressionTestCase( - id="bool_input", + "bool_input", doc={"arr0": True, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject bool input element", + msg="$zip should reject bool input element", ), ExpressionTestCase( - id="object_input", + "object_input", doc={"arr0": {"a": 1}, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject object input element", + msg="$zip should reject object input element", ), ExpressionTestCase( - id="double_input", + "double_input", doc={"arr0": 3.14, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject double input element", + msg="$zip should reject double input element", ), ExpressionTestCase( - id="negative_double_input", + "negative_double_input", doc={"arr0": -3.14, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject negative double input element", + msg="$zip should reject negative double input element", ), ExpressionTestCase( - id="decimal128_input", + "decimal128_input", doc={"arr0": Decimal128("1"), "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject decimal128 input element", + msg="$zip should reject decimal128 input element", ), ExpressionTestCase( - id="int64_input", + "int64_input", doc={"arr0": Int64(1), "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject int64 input element", + msg="$zip should reject int64 input element", ), ExpressionTestCase( - id="objectid_input", + "objectid_input", doc={"arr0": ObjectId(), "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject objectid input element", + msg="$zip should reject objectid input element", ), ExpressionTestCase( - id="datetime_input", + "datetime_input", doc={"arr0": datetime(2024, 1, 1, tzinfo=timezone.utc), "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject datetime input element", + msg="$zip should reject datetime input element", ), ExpressionTestCase( - id="binary_input", + "binary_input", doc={"arr0": Binary(b"x", 0), "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject binary input element", + msg="$zip should reject binary input element", ), ExpressionTestCase( - id="regex_input", + "regex_input", doc={"arr0": Regex("x"), "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject regex input element", + msg="$zip should reject regex input element", ), ExpressionTestCase( - id="maxkey_input", + "maxkey_input", doc={"arr0": MaxKey(), "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject maxkey input element", + msg="$zip should reject maxkey input element", ), ExpressionTestCase( - id="minkey_input", + "minkey_input", doc={"arr0": MinKey(), "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject minkey input element", + msg="$zip should reject minkey input element", ), ExpressionTestCase( - id="timestamp_input", + "timestamp_input", doc={"arr0": Timestamp(0, 0), "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject timestamp input element", + msg="$zip should reject timestamp input element", ), ExpressionTestCase( - id="non_array_second_position", + "non_array_second_position", doc={"arr0": [1], "arr1": 42}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject non-array in second position", + msg="$zip should reject non-array in second position", ), ExpressionTestCase( - id="non_array_middle_position", + "non_array_middle_position", doc={"arr0": [1], "arr1": "bad", "arr2": [2]}, expression={"$zip": {"inputs": ["$arr0", "$arr1", "$arr2"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject non-array in middle position", + msg="$zip should reject non-array in middle position", ), ] -# Error: special float/Decimal128 values as input element +# Property [Special Numeric Input]: $map rejects special numeric values as input. as input element SPECIAL_NUMERIC_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nan_input", + "nan_input", doc={"arr0": FLOAT_NAN, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject NaN input element", + msg="$zip should reject NaN input element", ), ExpressionTestCase( - id="inf_input", + "inf_input", doc={"arr0": FLOAT_INFINITY, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject Infinity input element", + msg="$zip should reject Infinity input element", ), ExpressionTestCase( - id="neg_inf_input", + "neg_inf_input", doc={"arr0": FLOAT_NEGATIVE_INFINITY, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject -Infinity input element", + msg="$zip should reject -Infinity input element", ), ExpressionTestCase( - id="neg_zero_input", + "neg_zero_input", doc={"arr0": DOUBLE_NEGATIVE_ZERO, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject negative zero input element", + msg="$zip should reject negative zero input element", ), ExpressionTestCase( - id="decimal128_nan_input", + "decimal128_nan_input", doc={"arr0": DECIMAL128_NAN, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject Decimal128 NaN input element", + msg="$zip should reject Decimal128 NaN input element", ), ExpressionTestCase( - id="decimal128_inf_input", + "decimal128_inf_input", doc={"arr0": DECIMAL128_INFINITY, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject Decimal128 Infinity input element", + msg="$zip should reject Decimal128 Infinity input element", ), ExpressionTestCase( - id="decimal128_neg_inf_input", + "decimal128_neg_inf_input", doc={"arr0": DECIMAL128_NEGATIVE_INFINITY, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject Decimal128 -Infinity input element", + msg="$zip should reject Decimal128 -Infinity input element", ), ExpressionTestCase( - id="decimal128_neg_zero_input", + "decimal128_neg_zero_input", doc={"arr0": DECIMAL128_NEGATIVE_ZERO, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject Decimal128 -0 input element", + msg="$zip should reject Decimal128 -0 input element", ), ] -# Error: numeric boundary values as input element +# Property [Boundary Input]: $map rejects numeric boundary values as input. as input element BOUNDARY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="int32_max_input", + "int32_max_input", doc={"arr0": INT32_MAX, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject INT32_MAX input element", + msg="$zip should reject INT32_MAX input element", ), ExpressionTestCase( - id="int32_min_input", + "int32_min_input", doc={"arr0": INT32_MIN, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject INT32_MIN input element", + msg="$zip should reject INT32_MIN input element", ), ExpressionTestCase( - id="int64_max_input", + "int64_max_input", doc={"arr0": INT64_MAX, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject INT64_MAX input element", + msg="$zip should reject INT64_MAX input element", ), ExpressionTestCase( - id="int64_min_input", + "int64_min_input", doc={"arr0": INT64_MIN, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject INT64_MIN input element", + msg="$zip should reject INT64_MIN input element", ), ExpressionTestCase( - id="decimal128_max_input", + "decimal128_max_input", doc={"arr0": DECIMAL128_MAX, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject DECIMAL128_MAX input element", + msg="$zip should reject DECIMAL128_MAX input element", ), ExpressionTestCase( - id="decimal128_min_input", + "decimal128_min_input", doc={"arr0": DECIMAL128_MIN, "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject DECIMAL128_MIN input element", + msg="$zip should reject DECIMAL128_MIN input element", ), ] -# Error: string edge cases as input element +# Property [String Element]: $zip rejects string values as input elements. STRING_EDGE_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="comma_separated_string_input", + "comma_separated_string_input", doc={"arr0": "1, 2, 3", "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject comma-separated string", + msg="$zip should reject comma-separated string", ), ExpressionTestCase( - id="json_like_string_input", + "json_like_string_input", doc={"arr0": "[1, 2, 3]", "arr1": [1]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Should reject JSON-like string", + msg="$zip should reject JSON-like string", ), ] -# Error: invalid useLongestLength +# Property [Invalid useLongestLength]: $zip rejects invalid useLongestLength values. USE_LONGEST_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="use_longest_string", + "use_longest_string", doc={"arr0": [1], "arr1": [2]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": "true"}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, - msg="Should reject string useLongestLength", + msg="$zip should reject string useLongestLength", ), ExpressionTestCase( - id="use_longest_int", + "use_longest_int", doc={"arr0": [1], "arr1": [2]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": 1}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, - msg="Should reject int useLongestLength", + msg="$zip should reject int useLongestLength", ), ExpressionTestCase( - id="use_longest_int_0", + "use_longest_int_0", doc={"arr0": [1, 2]}, expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": 0}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, - msg="Int 0 should error (not bool)", + msg="$zip int 0 should error (not bool)", ), ExpressionTestCase( - id="use_longest_empty_string", + "use_longest_empty_string", doc={"arr0": [1, 2]}, expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": ""}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, - msg="Empty string should error (not bool)", + msg="$zip empty string should error (not bool)", ), ExpressionTestCase( - id="use_longest_array", + "use_longest_array", doc={"arr0": [1, 2]}, expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": []}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, - msg="Empty array should error (not bool)", + msg="$zip empty array should error (not bool)", ), ExpressionTestCase( - id="use_longest_object", + "use_longest_object", doc={"arr0": [1, 2]}, expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": {"a": 1}}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, - msg="Object should error (not bool)", + msg="$zip object should error (not bool)", ), ExpressionTestCase( - id="use_longest_nan", + "use_longest_nan", doc={"arr0": [1, 2]}, expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": float("nan")}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, - msg="NaN should error (not bool)", + msg="$zip naN should error (not bool)", ), ExpressionTestCase( - id="use_longest_infinity", + "use_longest_infinity", doc={"arr0": [1, 2]}, expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": float("inf")}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, - msg="Infinity should error (not bool)", + msg="$zip infinity should error (not bool)", ), ] -# Error: invalid defaults +# Property [Invalid Defaults]: $zip rejects invalid defaults values. DEFAULTS_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="defaults_without_use_longest", + "defaults_without_use_longest", doc={"arr0": [1, 2], "arr1": [3]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"], "defaults": [0, 0]}}, error_code=ZIP_DEFAULTS_WITHOUT_LONGEST_ERROR, - msg="Should reject defaults without useLongestLength", + msg="$zip should reject defaults without useLongestLength", ), ExpressionTestCase( - id="defaults_without_longest_false", + "defaults_without_longest_false", doc={"arr0": [1], "arr1": [2]}, expression={ "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": False, "defaults": [0, 0]} }, error_code=ZIP_DEFAULTS_WITHOUT_LONGEST_ERROR, - msg="defaults with useLongestLength false should error", + msg="$zip defaults with useLongestLength false should error", ), ExpressionTestCase( - id="defaults_length_mismatch", + "defaults_length_mismatch", doc={"arr0": [1, 2], "arr1": [3]}, expression={ "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0]} }, error_code=ZIP_DEFAULTS_LENGTH_MISMATCH_ERROR, - msg="Should reject defaults with wrong length", + msg="$zip should reject defaults with wrong length", ), ExpressionTestCase( - id="defaults_too_many", + "defaults_too_many", doc={"arr0": [1], "arr1": [2]}, expression={ "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": [0, 0, 0]} }, error_code=ZIP_DEFAULTS_LENGTH_MISMATCH_ERROR, - msg="Should reject defaults longer than inputs", + msg="$zip should reject defaults longer than inputs", ), ExpressionTestCase( - id="defaults_not_array", + "defaults_not_array", doc={"arr0": [1], "arr1": [2]}, expression={ "$zip": {"inputs": ["$arr0", "$arr1"], "useLongestLength": True, "defaults": "bad"} }, error_code=ZIP_DEFAULTS_NOT_ARRAY_ERROR, - msg="Should reject non-array defaults", + msg="$zip should reject non-array defaults", ), ExpressionTestCase( - id="defaults_not_array_object", + "defaults_not_array_object", doc={"arr0": [1]}, expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": True, "defaults": {"a": 1}}}, error_code=ZIP_DEFAULTS_NOT_ARRAY_ERROR, - msg="defaults as object should error", + msg="$zip defaults as object should error", ), ExpressionTestCase( - id="defaults_not_array_int", + "defaults_not_array_int", doc={"arr0": [1]}, expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": True, "defaults": 1}}, error_code=ZIP_DEFAULTS_NOT_ARRAY_ERROR, - msg="defaults as int should error", + msg="$zip defaults as int should error", ), ] -# Aggregate and test ALL_INPUT_ELEMENT_TESTS = ( NOT_ARRAY_ELEMENT_TESTS + SPECIAL_NUMERIC_ERROR_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py index f658d4b97..7142157bf 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py @@ -17,61 +17,61 @@ from documentdb_tests.framework.error_codes import ZIP_REQUIRES_ARRAY_ELEMENT_ERROR from documentdb_tests.framework.parametrize import pytest_params -# Field path lookups +# Property [Field Path Resolution]: $map resolves nested and composite field paths. # Property [Field Lookup]: $zip resolves field paths in expressions. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_field_path", + "nested_field_path", expression={"$zip": {"inputs": ["$a.b", "$a.c"]}}, doc={"a": {"b": [1, 2], "c": [3, 4]}}, expected=[[1, 3], [2, 4]], - msg="Should resolve nested field paths", + msg="$zip should resolve nested field paths", ), ExpressionTestCase( - id="deeply_nested_field", + "deeply_nested_field", expression={"$zip": {"inputs": ["$a.b.c", "$a.b.d"]}}, doc={"a": {"b": {"c": [10], "d": [20]}}}, expected=[[10, 20]], - msg="Should resolve deeply nested field paths", + msg="$zip should resolve deeply nested field paths", ), ExpressionTestCase( - id="nonexistent_field_null", + "nonexistent_field_null", expression={"$zip": {"inputs": ["$a.nonexistent", "$b"]}}, doc={"a": {"missing": 1}, "b": [1]}, expected=None, - msg="Non-existent field should propagate null", + msg="$zip non-existent field should propagate null", ), ExpressionTestCase( - id="same_field_twice", + "same_field_twice", expression={"$zip": {"inputs": ["$arr", "$arr"]}}, doc={"arr": [1, 2, 3]}, expected=[[1, 1], [2, 2], [3, 3]], - msg="Same field used twice should zip with itself", + msg="$zip same field used twice should zip with itself", ), ] -# Composite array paths +# Property [Composite Paths]: $zip resolves composite array paths from array-of-objects. COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="composite_array", + "composite_array", expression={"$zip": {"inputs": ["$x.y", "$x.z"]}}, doc={"x": [{"y": 1, "z": 10}, {"y": 2, "z": 20}]}, expected=[[1, 10], [2, 20]], - msg="Composite array path from array-of-objects", + msg="$zip composite array path from array-of-objects", ), ExpressionTestCase( - id="array_index_path_resolves_empty", + "array_index_path_resolves_empty", expression={"$zip": {"inputs": ["$a.0", [1, 2]]}}, doc={"a": [[1, 2], [3, 4]]}, expected=[], - msg="Array index path resolves to [] (shortest)", + msg="$zip array index path resolves to [] (shortest)", ), ] -# $let and system variables +# Property [Variables]: $map works with $let and system variables. LET_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="let_variable", + "let_variable", expression={ "$let": { "vars": {"a": "$arr1", "b": "$arr2"}, @@ -80,17 +80,17 @@ }, doc={"arr1": [1, 2], "arr2": [3, 4]}, expected=[[1, 3], [2, 4]], - msg="Should work with $let variables", + msg="$zip should work with $let variables", ), ExpressionTestCase( - id="root_variable", + "root_variable", expression={"$zip": {"inputs": ["$$ROOT.a", "$$ROOT.b"]}}, doc={"_id": 1, "a": [1], "b": [2]}, expected=[[1, 2]], - msg="Should work with $$ROOT", + msg="$zip should work with $$ROOT", ), ExpressionTestCase( - id="current_variable", + "current_variable", expression={"$zip": {"inputs": ["$$CURRENT.a", "$$CURRENT.b"]}}, doc={"_id": 2, "a": [1], "b": [2]}, expected=[[1, 2]], @@ -98,87 +98,87 @@ ), ] -# Null/missing via expression +# Property [Null/Missing Fields]: null and missing field paths produce null results. # Property [Null/Missing Fields]: $zip handles null and missing field paths. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="missing_field", + "missing_field", expression={"$zip": {"inputs": ["$nonexistent", [1]]}}, doc={"other": 1}, expected=None, - msg="Missing field should propagate null", + msg="$zip missing field should propagate null", ), ExpressionTestCase( - id="missing_input_type_is_null", + "missing_input_type_is_null", expression={"$type": {"$zip": {"inputs": ["$nonexistent", [1]]}}}, doc={"x": 1}, expected="null", - msg="Missing field should produce null type", + msg="$zip missing field should produce null type", ), ExpressionTestCase( - id="remove_variable", + "remove_variable", expression={"$zip": {"inputs": ["$$REMOVE", [1]]}}, doc={"x": 1}, expected=None, msg="$$REMOVE propagates null", ), ExpressionTestCase( - id="field_ref_non_array", + "field_ref_non_array", expression={"$zip": {"inputs": ["$a", [1]]}}, doc={"a": 1}, error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, - msg="Field resolving to non-array should error", + msg="$zip field resolving to non-array should error", ), ExpressionTestCase( - id="missing_first_input", + "missing_first_input", expression={"$zip": {"inputs": ["$missing", [1, 2]]}}, doc={"a": 1}, expected=None, - msg="Missing first input returns null", + msg="$zip missing first input returns null", ), ExpressionTestCase( - id="all_missing_fields", + "all_missing_fields", expression={"$zip": {"inputs": ["$x", "$y"]}}, doc={"_placeholder": 1}, expected=None, - msg="All missing fields return null", + msg="$zip all missing fields return null", ), ExpressionTestCase( - id="explicit_null_field", + "explicit_null_field", expression={"$zip": {"inputs": ["$a", "$b"]}}, doc={"a": None, "b": [1, 2]}, expected=None, - msg="Explicit null field returns null", + msg="$zip explicit null field returns null", ), ExpressionTestCase( - id="null_with_longest_true", + "null_with_longest_true", expression={"$zip": {"inputs": ["$a", [1, 2]], "useLongestLength": True}}, doc={"a": None}, expected=None, - msg="Null input with longest true returns null", + msg="$zip null input with longest true returns null", ), ExpressionTestCase( - id="null_with_defaults", + "null_with_defaults", expression={ "$zip": {"inputs": ["$a", [1, 2]], "useLongestLength": True, "defaults": [0, 0]} }, doc={"a": None}, expected=None, - msg="Null input with defaults still returns null", + msg="$zip null input with defaults still returns null", ), ExpressionTestCase( - id="missing_field_as_element", + "missing_field_as_element", expression={"$zip": {"inputs": [["$not_exist", 2], [1, 2]]}}, doc={"a": 1}, expected=[[None, 1], [2, 2]], - msg="Missing field as element becomes null", + msg="$zip missing field as element becomes null", ), ] -# Self-composition +# Property [Self-Composition]: $zip composes with nested $zip calls. SELF_COMPOSITION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="nested_zip_full", + "nested_zip_full", expression={ "$zip": { "inputs": [ @@ -189,38 +189,37 @@ }, doc={"a": [1, 2], "b": [3, 4], "c": ["x", "y"]}, expected=[[[1, 3], "x"], [[2, 4], "y"]], - msg="Nested $zip as input works correctly", + msg="$zip nested $zip as input works correctly", ), ] -# Field path as element in input array +# Property [Field Path Elements]: $zip resolves field paths used as array elements. FIELD_PATH_ELEMENT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="field_path_as_element", + "field_path_as_element", expression={"$zip": {"inputs": [["$a", "$b"], [1, 2]]}}, doc={"a": 10, "b": 20}, expected=[[10, 1], [20, 2]], - msg="Field paths as elements resolve correctly", + msg="$zip field paths as elements resolve correctly", ), ExpressionTestCase( - id="nested_field_path_as_element", + "nested_field_path_as_element", expression={"$zip": {"inputs": [["$foo.bar", "$b"], [1, 2]]}}, doc={"foo": {"bar": 10}, "b": 20}, expected=[[10, 1], [20, 2]], - msg="Nested field path as element resolves correctly", + msg="$zip nested field path as element resolves correctly", ), ExpressionTestCase( - id="field_path_in_defaults", + "field_path_in_defaults", expression={ "$zip": {"inputs": ["$a", "$b"], "useLongestLength": True, "defaults": ["$c", "$d"]} }, doc={"a": [1, 2, 3], "b": ["x"], "c": "default_a", "d": "default_b"}, expected=[[1, "x"], [2, "default_b"], [3, "default_b"]], - msg="Field paths in defaults resolve from document", + msg="$zip field paths in defaults resolve from document", ), ] -# Aggregate and test ALL_EXPR_TESTS = ( FIELD_LOOKUP_TESTS + COMPOSITE_PATH_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_map.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_map.py index a59530606..465c12f31 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_map.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_map.py @@ -15,16 +15,16 @@ MAP_COMBINATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="map_on_range", + "map_on_range", expression={ "$map": {"input": {"$range": [0, 5]}, "in": {"$multiply": ["$$this", "$$this"]}} }, doc={"_placeholder": 1}, expected=[0, 1, 4, 9, 16], - msg="Should map squares over $range result", + msg="$map should map squares over $range result", ), ExpressionTestCase( - id="map_with_concatArrays", + "map_with_concatArrays", expression={ "$concatArrays": [ {"$map": {"input": "$a", "in": {"$multiply": ["$$this", 2]}}}, @@ -33,10 +33,10 @@ }, doc={"a": [1, 2], "b": [3, 4]}, expected=[2, 4, 9, 12], - msg="Should concatenate two mapped arrays", + msg="$map should concatenate two mapped arrays", ), ExpressionTestCase( - id="map_with_filter", + "map_with_filter", expression={ "$map": { "input": {"$filter": {"input": "$arr", "cond": {"$gt": ["$$this", 2]}}}, @@ -45,10 +45,10 @@ }, doc={"arr": [1, 2, 3, 4, 5]}, expected=[30, 40, 50], - msg="Should map over filtered array", + msg="$map should map over filtered array", ), ExpressionTestCase( - id="map_result_into_reduce", + "map_result_into_reduce", expression={ "$reduce": { "input": {"$map": {"input": "$arr", "in": {"$multiply": ["$$this", 2]}}}, @@ -61,7 +61,7 @@ msg="$reduce on $map result should sum doubled values", ), ExpressionTestCase( - id="map_3_level_nested", + "map_3_level_nested", expression={ "$map": { "input": [[[1]]], @@ -82,7 +82,7 @@ }, doc={"_placeholder": 1}, expected=[[[101]]], - msg="3-level nested $map should work", + msg="$map 3-level nested $map should work", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_range.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_range.py index 92b911224..8ad13a7ff 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_range.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_range.py @@ -15,67 +15,67 @@ RANGE_COMBINATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="reverseArray_on_range", + "reverseArray_on_range", expression={"$reverseArray": {"$range": ["$start", "$end"]}}, doc={"start": 0, "end": 5}, expected=[4, 3, 2, 1, 0], msg="$reverseArray on $range result", ), ExpressionTestCase( - id="concatArrays_two_ranges", + "concatArrays_two_ranges", expression={"$concatArrays": [{"$range": [0, 3]}, {"$range": [3, 6]}]}, doc={"x": 1}, expected=[0, 1, 2, 3, 4, 5], msg="$concatArrays on two $range results", ), ExpressionTestCase( - id="in_on_range", + "in_on_range", expression={"$in": [5, {"$range": ["$start", "$end"]}]}, doc={"start": 0, "end": 10}, expected=True, msg="$in on $range result", ), ExpressionTestCase( - id="isArray_on_range", + "isArray_on_range", expression={"$isArray": {"$range": [0, 3]}}, doc={"x": 1}, expected=True, msg="$isArray on $range result should return true", ), ExpressionTestCase( - id="in_miss_on_range", + "in_miss_on_range", expression={"$in": [5, {"$range": [0, 5]}]}, doc={"x": 1}, expected=False, - msg="5 should not be in [0..4] (exclusive end)", + msg="$range 5 should not be in [0..4] (exclusive end)", ), ExpressionTestCase( - id="self_nesting_start", + "self_nesting_start", expression={"$range": [{"$arrayElemAt": [{"$range": [2, 5]}, 0]}, 10]}, doc={"x": 1}, expected=[2, 3, 4, 5, 6, 7, 8, 9], - msg="Start from inner range", + msg="$range start from inner range", ), ExpressionTestCase( - id="self_nesting_end", + "self_nesting_end", expression={"$range": [0, {"$size": {"$range": [0, 5]}}]}, doc={"x": 1}, expected=[0, 1, 2, 3, 4], - msg="End from size of inner range", + msg="$range end from size of inner range", ), ExpressionTestCase( - id="indexOfArray_on_range", + "indexOfArray_on_range", expression={"$indexOfArray": [{"$range": [0, 10]}, 7]}, doc={"x": 1}, expected=7, - msg="Index of 7 in range 0..9 should be 7", + msg="$range index of 7 in range 0..9 should be 7", ), ExpressionTestCase( - id="output_type_is_int", + "output_type_is_int", expression={"$type": {"$arrayElemAt": [{"$range": [0, 1]}, 0]}}, doc={"x": 1}, expected="int", - msg="Output element should be int type", + msg="$range output element should be int type", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_zip.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_zip.py index 00058490e..0ac70e1e5 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_zip.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_zip.py @@ -16,35 +16,35 @@ ZIP_COMBINATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( - id="zip_on_sortArray", + "zip_on_sortArray", expression={"$zip": {"inputs": [{"$sortArray": {"input": "$a", "sortBy": 1}}, "$b"]}}, doc={"a": [3, 1, 2], "b": [10, 20, 30]}, expected=[[1, 10], [2, 20], [3, 30]], - msg="Should zip $sortArray result with array", + msg="$zip should zip $sortArray result with array", ), ExpressionTestCase( - id="zip_on_concatArrays", + "zip_on_concatArrays", expression={"$zip": {"inputs": [{"$concatArrays": ["$a", "$b"]}, "$c"]}}, doc={"a": [1], "b": [2], "c": [10, 20]}, expected=[[1, 10], [2, 20]], - msg="Should zip $concatArrays result with array", + msg="$zip should zip $concatArrays result with array", ), ExpressionTestCase( - id="zip_on_range", + "zip_on_range", expression={"$zip": {"inputs": [{"$range": [0, 3]}, {"$range": [10, 13]}]}}, doc={"x": 1}, expected=[[0, 10], [1, 11], [2, 12]], - msg="Should zip two $range results", + msg="$zip should zip two $range results", ), ExpressionTestCase( - id="arrayElemAt_on_zip", + "arrayElemAt_on_zip", expression={"$arrayElemAt": [{"$zip": {"inputs": ["$a", "$b"]}}, 1]}, doc={"a": [1, 2, 3], "b": [10, 20, 30]}, expected=[2, 20], msg="$arrayElemAt on $zip result", ), ExpressionTestCase( - id="zip_matrix_transpose_2x3", + "zip_matrix_transpose_2x3", expression={ "$zip": { "inputs": [ @@ -55,17 +55,17 @@ }, doc={"matrix": [[0, 1, 2], [3, 4, 5]]}, expected=[[0, 3], [1, 4], [2, 5]], - msg="2x3 matrix transposed to 3x2", + msg="$zip 2x3 matrix transposed to 3x2", ), ExpressionTestCase( - id="zip_index_preservation", + "zip_index_preservation", expression={"$zip": {"inputs": ["$arr", {"$range": [0, {"$size": "$arr"}]}]}}, doc={"arr": ["a", "b", "c"]}, expected=[["a", 0], ["b", 1], ["c", 2]], - msg="Elements paired with indices", + msg="$zip elements paired with indices", ), ExpressionTestCase( - id="zip_reduce_on_output", + "zip_reduce_on_output", expression={ "$reduce": { "input": {"$zip": {"inputs": ["$a", "$b"]}}, @@ -78,21 +78,21 @@ msg="$reduce sums second elements of zipped pairs", ), ExpressionTestCase( - id="zip_output_is_array", + "zip_output_is_array", expression={"$isArray": {"$zip": {"inputs": ["$a", "$b"]}}}, doc={"a": [1, 2], "b": ["a", "b"]}, expected=True, - msg="Output is an array", + msg="$zip output is an array", ), ExpressionTestCase( - id="float_nan_preserved", + "float_nan_preserved", expression={"$arrayElemAt": [{"$arrayElemAt": [{"$zip": {"inputs": ["$a", "$b"]}}, 0]}, 0]}, doc={"a": [FLOAT_NAN], "b": [1]}, expected=pytest.approx(FLOAT_NAN, nan_ok=True), - msg="Float NaN element preserved after zipping", + msg="$zip float NaN element preserved after zipping", ), ExpressionTestCase( - id="default_float_nan", + "default_float_nan", expression={ "$arrayElemAt": [ { @@ -112,7 +112,7 @@ }, doc={"a": [1, 2], "b": [10]}, expected=pytest.approx(FLOAT_NAN, nan_ok=True), - msg="Should use float NaN as default value", + msg="$zip should use float NaN as default value", ), ] From 212e731b861de90da4189b6fb7ab9ee571a3ae9b Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 9 Jul 2026 15:47:03 -0700 Subject: [PATCH 11/17] use constants Signed-off-by: Alina (Xi) Li --- .../array/map/test_map_as_errors.py | 5 +-- .../array/map/test_map_bson_types.py | 15 +++++---- .../expressions/array/map/test_map_errors.py | 3 +- .../array/map/test_map_expressions.py | 3 +- .../range/test_range_numeric_acceptance.py | 21 ++++++++---- .../array/range/test_range_value_errors.py | 33 ++++++++++++------- .../array/zip/test_zip_bson_types.py | 18 ++++++---- .../expressions/array/zip/test_zip_errors.py | 4 +-- .../array/zip/test_zip_expressions.py | 3 +- 9 files changed, 66 insertions(+), 39 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py index 91c555023..bd71b0059 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py @@ -18,6 +18,7 @@ ) 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 # Property [Invalid As Type]: $map rejects non-string types for the as parameter. INVALID_AS_TYPE_TESTS: list[ExpressionTestCase] = [ @@ -107,13 +108,13 @@ ), ExpressionTestCase( "type_nan", - expression={"$map": {"input": [1, 2, 3], "as": float("nan"), "in": "$$this"}}, + expression={"$map": {"input": [1, 2, 3], "as": FLOAT_NAN, "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, msg="$map should reject NaN as variable name", ), ExpressionTestCase( "type_infinity", - expression={"$map": {"input": [1, 2, 3], "as": float("inf"), "in": "$$this"}}, + expression={"$map": {"input": [1, 2, 3], "as": FLOAT_INFINITY, "in": "$$this"}}, error_code=FAILED_TO_PARSE_ERROR, msg="$map should reject Infinity as variable name", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py index 2fb12b020..69a765390 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py @@ -24,6 +24,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={"$map": {"input": "$arr", "in": "$$this"}}, - 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="$map should preserve Decimal128 values", ), ExpressionTestCase( @@ -189,8 +192,8 @@ ExpressionTestCase( "decimal128_trailing_zeros", expression={"$map": {"input": "$arr", "in": "$$this"}}, - 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="$map decimal128 trailing zeros preserved", ), ExpressionTestCase( @@ -215,7 +218,7 @@ ExpressionTestCase( "add_decimal128", expression={"$map": {"input": "$arr", "in": {"$add": ["$$this", Decimal128("0.1")]}}}, - doc={"arr": [Decimal128("1.5"), Decimal128("2.5"), Decimal128("3.5")]}, + doc={"arr": [DECIMAL128_ONE_AND_HALF, DECIMAL128_TWO_AND_HALF, Decimal128("3.5")]}, expected=[Decimal128("1.6"), Decimal128("2.6"), Decimal128("3.6")], msg="$add on Decimal128 should preserve precision", ), @@ -260,7 +263,7 @@ ExpressionTestCase( "toDouble_decimal128", expression={"$map": {"input": "$arr", "in": {"$toDouble": "$$this"}}}, - doc={"arr": [Decimal128("1.5"), Decimal128("2.0")]}, + doc={"arr": [DECIMAL128_ONE_AND_HALF, Decimal128("2.0")]}, expected=[1.5, 2.0], msg="$toDouble on Decimal128 array", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py index 2750c6fee..e82a6ba97 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py @@ -28,6 +28,7 @@ DECIMAL128_MIN, DECIMAL128_NAN, DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_NAN, DECIMAL128_NEGATIVE_ZERO, DOUBLE_NEGATIVE_ZERO, FLOAT_INFINITY, @@ -195,7 +196,7 @@ ExpressionTestCase( "decimal128_neg_nan_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, - doc={"arr": Decimal128("-NaN")}, + doc={"arr": DECIMAL128_NEGATIVE_NAN}, error_code=MAP_INPUT_NOT_ARRAY_ERROR, msg="$map should reject Decimal128 -NaN input", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py index b841a2345..50d899d02 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py @@ -15,6 +15,7 @@ execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import MISSING # Property [Field Path Resolution]: $map resolves nested and composite field paths. # Property [Field Lookup]: $map resolves field paths in expressions. @@ -126,7 +127,7 @@ ), ExpressionTestCase( "missing_field_in_expression", - expression={"$map": {"input": "$arr", "in": "$missing"}}, + expression={"$map": {"input": "$arr", "in": MISSING}}, doc={"arr": [1, 2, 3]}, expected=[None, None, None], msg="$map missing field in 'in' should produce null for each element", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py index 1a52f5c1e..47dea5463 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py @@ -16,26 +16,33 @@ execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ZERO, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_ZERO, + INT64_ZERO, +) # Property [Numeric Type Acceptance]: $range accepts Int64, whole doubles, and whole Decimal128. NUMERIC_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "int64_args", - doc={"start": Int64(0), "end": Int64(3)}, + doc={"start": INT64_ZERO, "end": Int64(3)}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], msg="$range should accept Int64 arguments", ), ExpressionTestCase( "whole_double_args", - doc={"start": 0.0, "end": 3.0}, + doc={"start": DOUBLE_ZERO, "end": 3.0}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], msg="$range should accept whole-number double arguments", ), ExpressionTestCase( "whole_decimal128_args", - doc={"start": Decimal128("0"), "end": Decimal128("3")}, + doc={"start": DECIMAL128_ZERO, "end": Decimal128("3")}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2], msg="$range should accept whole-number Decimal128 arguments", @@ -178,28 +185,28 @@ NEG_ZERO_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "neg_zero_double_start", - doc={"start": -0.0, "end": 5}, + doc={"start": DOUBLE_NEGATIVE_ZERO, "end": 5}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2, 3, 4], msg="$range negative zero double start treated as 0", ), ExpressionTestCase( "neg_zero_decimal_start", - doc={"start": Decimal128("-0"), "end": 5}, + doc={"start": DECIMAL128_NEGATIVE_ZERO, "end": 5}, expression={"$range": ["$start", "$end"]}, expected=[0, 1, 2, 3, 4], msg="$range negative zero Decimal128 start treated as 0", ), ExpressionTestCase( "neg_zero_double_end", - doc={"start": 0, "end": -0.0}, + doc={"start": 0, "end": DOUBLE_NEGATIVE_ZERO}, expression={"$range": ["$start", "$end"]}, expected=[], msg="$range negative zero double end treated as 0", ), ExpressionTestCase( "neg_zero_decimal_end", - doc={"start": 0, "end": Decimal128("-0")}, + doc={"start": 0, "end": DECIMAL128_NEGATIVE_ZERO}, expression={"$range": ["$start", "$end"]}, expected=[], msg="$range negative zero Decimal128 end treated as 0", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py index f0704fa1b..e49e7c57a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py @@ -6,7 +6,7 @@ """ import pytest -from bson import Decimal128, Int64 +from bson import Decimal128 from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 ExpressionTestCase, @@ -25,9 +25,17 @@ ) from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( + DECIMAL128_HALF, DECIMAL128_INFINITY, DECIMAL128_NAN, DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_NAN, + DECIMAL128_NEGATIVE_ONE_AND_HALF, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ONE_AND_HALF, + DECIMAL128_ZERO, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_ZERO, FLOAT_INFINITY, FLOAT_NAN, FLOAT_NEGATIVE_INFINITY, @@ -35,6 +43,7 @@ INT32_UNDERFLOW, INT64_MAX, INT64_MIN, + INT64_ZERO, ) # Property [Non-Integral Start]: $range rejects fractional numeric values for start. @@ -48,7 +57,7 @@ ), ExpressionTestCase( "decimal128_fractional_start", - doc={"start": Decimal128("0.5"), "end": 5}, + doc={"start": DECIMAL128_HALF, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="$range should reject fractional Decimal128 start", @@ -62,14 +71,14 @@ ), ExpressionTestCase( "decimal128_negative_fractional_start", - doc={"start": Decimal128("-1.5"), "end": 5}, + doc={"start": DECIMAL128_NEGATIVE_ONE_AND_HALF, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="$range should reject negative fractional Decimal128 start", ), ExpressionTestCase( "decimal128_negative_nan_start", - doc={"start": Decimal128("-NaN"), "end": 5}, + doc={"start": DECIMAL128_NEGATIVE_NAN, "end": 5}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_START_NOT_INTEGRAL_ERROR, msg="$range should reject Decimal128 -NaN start", @@ -101,7 +110,7 @@ ), ExpressionTestCase( "decimal128_negative_fractional_end", - doc={"start": 0, "end": Decimal128("-1.5")}, + doc={"start": 0, "end": DECIMAL128_NEGATIVE_ONE_AND_HALF}, expression={"$range": ["$start", "$end"]}, error_code=RANGE_END_NOT_INT32_ERROR, msg="$range should reject negative fractional Decimal128 end", @@ -119,7 +128,7 @@ ), ExpressionTestCase( "decimal128_fractional_step", - doc={"start": 0, "end": 10, "step": Decimal128("1.5")}, + doc={"start": 0, "end": 10, "step": DECIMAL128_ONE_AND_HALF}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, msg="$range should reject fractional Decimal128 step", @@ -133,7 +142,7 @@ ), ExpressionTestCase( "decimal128_negative_fractional_step", - doc={"start": 10, "end": 0, "step": Decimal128("-1.5")}, + doc={"start": 10, "end": 0, "step": DECIMAL128_NEGATIVE_ONE_AND_HALF}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_NOT_INT32_ERROR, msg="$range should reject negative fractional Decimal128 step", @@ -212,35 +221,35 @@ ), ExpressionTestCase( "step_zero_int64", - doc={"start": 0, "end": 5, "step": Int64(0)}, + doc={"start": 0, "end": 5, "step": INT64_ZERO}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, msg="$range should reject Int64 step 0", ), ExpressionTestCase( "step_zero_double", - doc={"start": 0, "end": 5, "step": 0.0}, + doc={"start": 0, "end": 5, "step": DOUBLE_ZERO}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, msg="$range should reject double step 0.0", ), ExpressionTestCase( "step_zero_decimal128", - doc={"start": 0, "end": 5, "step": Decimal128("0")}, + doc={"start": 0, "end": 5, "step": DECIMAL128_ZERO}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, msg="$range should reject Decimal128 step 0", ), ExpressionTestCase( "step_neg_zero_double", - doc={"start": 0, "end": 5, "step": -0.0}, + doc={"start": 0, "end": 5, "step": DOUBLE_NEGATIVE_ZERO}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, msg="$range should reject negative zero double step", ), ExpressionTestCase( "step_neg_zero_decimal128", - doc={"start": 0, "end": 5, "step": Decimal128("-0")}, + doc={"start": 0, "end": 5, "step": DECIMAL128_NEGATIVE_ZERO}, expression={"$range": ["$start", "$end", "$step"]}, error_code=RANGE_STEP_ZERO_ERROR, msg="$range should reject negative zero Decimal128 step", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py index 252a45910..2f62fcf08 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py @@ -23,6 +23,9 @@ DECIMAL128_INFINITY, DECIMAL128_NAN, DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_ONE_AND_HALF, + DECIMAL128_TWO_AND_HALF, + DECIMAL128_ZERO, DOUBLE_NEGATIVE_ZERO, FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY, @@ -30,6 +33,7 @@ INT32_MIN, INT64_MAX, INT64_MIN, + INT64_ZERO, ) # Property [Type Preservation]: $zip preserves each element BSON type. @@ -44,9 +48,9 @@ ), ExpressionTestCase( "decimal128_values", - doc={"arr0": [Decimal128("1.5")], "arr1": [Decimal128("2.5")]}, + doc={"arr0": [DECIMAL128_ONE_AND_HALF], "arr1": [DECIMAL128_TWO_AND_HALF]}, expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, - expected=[[Decimal128("1.5"), Decimal128("2.5")]], + expected=[[DECIMAL128_ONE_AND_HALF, DECIMAL128_TWO_AND_HALF]], msg="$zip should preserve Decimal128 values", ), ExpressionTestCase( @@ -177,23 +181,23 @@ "$zip": { "inputs": ["$arr0", "$arr1"], "useLongestLength": True, - "defaults": [0, Int64(0)], + "defaults": [0, INT64_ZERO], } }, - expected=[[1, Int64(10)], [2, Int64(0)], [3, Int64(0)]], + expected=[[1, Int64(10)], [2, INT64_ZERO], [3, INT64_ZERO]], msg="$zip should use Int64 default value", ), ExpressionTestCase( "default_decimal128", - doc={"arr0": [1, 2, 3], "arr1": [Decimal128("1.5")]}, + doc={"arr0": [1, 2, 3], "arr1": [DECIMAL128_ONE_AND_HALF]}, expression={ "$zip": { "inputs": ["$arr0", "$arr1"], "useLongestLength": True, - "defaults": [0, Decimal128("0")], + "defaults": [0, DECIMAL128_ZERO], } }, - expected=[[1, Decimal128("1.5")], [2, Decimal128("0")], [3, Decimal128("0")]], + expected=[[1, DECIMAL128_ONE_AND_HALF], [2, DECIMAL128_ZERO], [3, DECIMAL128_ZERO]], msg="$zip should use Decimal128 default value", ), ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py index 9bb6bff7c..ed4088fff 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py @@ -344,14 +344,14 @@ ExpressionTestCase( "use_longest_nan", doc={"arr0": [1, 2]}, - expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": float("nan")}}, + expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": FLOAT_NAN}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, msg="$zip naN should error (not bool)", ), ExpressionTestCase( "use_longest_infinity", doc={"arr0": [1, 2]}, - expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": float("inf")}}, + expression={"$zip": {"inputs": ["$arr0"], "useLongestLength": FLOAT_INFINITY}}, error_code=ZIP_USE_LONGEST_NOT_BOOL_ERROR, msg="$zip infinity should error (not bool)", ), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py index 7142157bf..b013cef90 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py @@ -16,6 +16,7 @@ ) from documentdb_tests.framework.error_codes import ZIP_REQUIRES_ARRAY_ELEMENT_ERROR from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import MISSING # Property [Field Path Resolution]: $map resolves nested and composite field paths. # Property [Field Lookup]: $zip resolves field paths in expressions. @@ -131,7 +132,7 @@ ), ExpressionTestCase( "missing_first_input", - expression={"$zip": {"inputs": ["$missing", [1, 2]]}}, + expression={"$zip": {"inputs": [MISSING, [1, 2]]}}, doc={"a": 1}, expected=None, msg="$zip missing first input returns null", From c251e0fd2fac80f96e2f409eaa46a6e474091047 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Thu, 9 Jul 2026 16:40:18 -0700 Subject: [PATCH 12/17] fix comments Signed-off-by: Alina (Xi) Li --- .../operator/expressions/array/map/test_map_bson_types.py | 2 -- .../operator/expressions/array/map/test_map_core_behavior.py | 3 --- .../operator/expressions/array/map/test_map_expressions.py | 3 --- .../expressions/array/map/test_map_structure_errors.py | 2 -- .../expressions/array/range/test_range_core_behavior.py | 1 - .../expressions/array/range/test_range_expressions.py | 4 ++-- .../expressions/array/range/test_range_numeric_acceptance.py | 1 - .../expressions/array/range/test_range_value_errors.py | 1 - .../operator/expressions/array/zip/test_zip_bson_types.py | 3 --- .../operator/expressions/array/zip/test_zip_core_behavior.py | 2 -- .../expressions/array/zip/test_zip_degenerate_cases.py | 2 -- .../core/operator/expressions/array/zip/test_zip_errors.py | 4 ++-- .../operator/expressions/array/zip/test_zip_expressions.py | 5 ++--- 13 files changed, 6 insertions(+), 27 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py index 69a765390..70781de24 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_bson_types.py @@ -36,7 +36,6 @@ INT64_MIN, ) -# Property [Type Preservation]: $map preserves each element BSON type. # Property [Type Preservation]: $map preserves each element's BSON type. BSON_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -205,7 +204,6 @@ ), ] -# Property [Type Transforms]: $map applies type-specific operator transformations. # Property [Type Transform]: $map transforms elements using type-specific operations. BSON_TRANSFORM_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py index 5f1155939..49c823396 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_core_behavior.py @@ -17,7 +17,6 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# Property [Basic Transform]: $map applies an expression to each array element. # Property [Basic Transform]: $map applies an expression to each element. BASIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -92,7 +91,6 @@ ), ] -# Property [Nested Arrays]: $map does not flatten nested array structures. # Property [Nested Arrays]: $map operates on nested array structures. NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -172,7 +170,6 @@ ), ] -# Property [Large Arrays]: $map handles arrays with many elements. # Property [Large Arrays]: $map handles large arrays. LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py index 50d899d02..c983ce8c0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_expressions.py @@ -101,7 +101,6 @@ ), ] -# Property [Null/Missing Fields]: null and missing field paths produce null results. # Property [Null/Missing Fields]: $map handles null and missing field paths. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -158,7 +157,6 @@ ] -# Property [Reduce Interaction]: $map composes correctly with $reduce scoping. # Property [Reduce Interaction]: $map output works with $reduce. REDUCE_INTERACTION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -197,7 +195,6 @@ ] # Property [System Variables]: $map works with $$ROOT and $$REMOVE. -# Property [System Variables]: $map works with $$ROOT and $$CURRENT. SYSTEM_VAR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "root_returns_full_doc", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py index 4a427fd5f..9e34ee16c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_structure_errors.py @@ -56,7 +56,6 @@ ), ] -# Property [Unknown Fields]: $map rejects unrecognized fields in the argument object. # Property [Unknown Fields]: $map rejects unknown fields in the argument. UNKNOWN_FIELD_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -73,7 +72,6 @@ ), ] -# Property [Required Fields]: $map rejects when required fields are missing. # Property [Required Fields]: $map requires the input and in fields. MISSING_REQUIRED_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py index 7720fac92..df99838b3 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_core_behavior.py @@ -56,7 +56,6 @@ ] # Property [Custom Step]: $range respects custom step values. -# Property [Step]: $range respects custom step values. STEP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "step_two", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py index f23d961e6..b39f4cc31 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_expressions.py @@ -21,7 +21,7 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# Property [Field Path Resolution]: $map resolves nested and composite field paths. +# Property [Field Path Resolution]: $range resolves nested and composite field paths. # Property [Field Lookup]: $range resolves field paths in expressions. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -47,7 +47,7 @@ ), ] -# Property [Variables]: $map works with $let and system variables. +# Property [Variables]: $range works with $let and system variables. LET_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "let_variable", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py index 47dea5463..5e4a1b540 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_numeric_acceptance.py @@ -91,7 +91,6 @@ ), ] -# Property [Single Element]: $range produces single-element arrays at boundaries. # Property [Single Element]: $range produces single-element arrays. SINGLE_ELEMENT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py index e49e7c57a..a3e674452 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_value_errors.py @@ -149,7 +149,6 @@ ), ] -# Property [Special Numeric Values]: $range rejects NaN and Infinity values. # Property [Special Numerics]: $range rejects NaN and Infinity values. SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py index 2f62fcf08..4cd5c04c9 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py @@ -36,7 +36,6 @@ INT64_ZERO, ) -# Property [Type Preservation]: $zip preserves each element BSON type. # Property [Type Preservation]: $zip preserves each element's BSON type. BSON_TYPE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -120,7 +119,6 @@ ), ] -# Property [Mixed Types]: $map processes arrays with mixed BSON types. across arrays # Property [Mixed Types]: $zip processes arrays with mixed BSON types. MIXED_BSON_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -132,7 +130,6 @@ ), ] -# Property [Special Numerics]: $map preserves NaN, Infinity, and boundary values. # Property [Special Numerics]: $zip preserves NaN, Infinity, and boundary values. SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py index 5d6dbc45c..33468aaa7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_core_behavior.py @@ -56,7 +56,6 @@ ), ] -# Property [Unequal Length]: $zip truncates to the shortest array by default. # Property [Unequal Length]: $zip truncates to the shortest array. UNEQUAL_LENGTH_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -114,7 +113,6 @@ ), ] -# Property [Defaults]: $zip pads shorter arrays with specified default values. # Property [Defaults]: $zip pads shorter arrays with default values. DEFAULTS_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py index ba0cf6778..5a8c63ba5 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py @@ -78,7 +78,6 @@ ] # Property [Nested Arrays]: $zip preserves nested array and object elements. -# Property [Nested Arrays]: $zip operates on nested array structures. NESTED_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "nested_arrays", @@ -146,7 +145,6 @@ ), ] -# Property [Large Arrays]: $map handles arrays with many elements.s # Property [Large Arrays]: $zip handles large arrays. LARGE_ARRAY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py index ed4088fff..7190ddb8f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py @@ -173,7 +173,7 @@ ), ] -# Property [Special Numeric Input]: $map rejects special numeric values as input. as input element +# Property [Special Numeric Input]: $zip rejects special numeric values as input. SPECIAL_NUMERIC_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "nan_input", @@ -233,7 +233,7 @@ ), ] -# Property [Boundary Input]: $map rejects numeric boundary values as input. as input element +# Property [Boundary Input]: $zip rejects numeric boundary values as input. BOUNDARY_ERROR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "int32_max_input", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py index b013cef90..0e65c949a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_expressions.py @@ -18,7 +18,7 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import MISSING -# Property [Field Path Resolution]: $map resolves nested and composite field paths. +# Property [Field Path Resolution]: $zip resolves nested and composite field paths. # Property [Field Lookup]: $zip resolves field paths in expressions. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -69,7 +69,7 @@ ), ] -# Property [Variables]: $map works with $let and system variables. +# Property [Variables]: $zip works with $let and system variables. LET_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "let_variable", @@ -99,7 +99,6 @@ ), ] -# Property [Null/Missing Fields]: null and missing field paths produce null results. # Property [Null/Missing Fields]: $zip handles null and missing field paths. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( From 4f42fbb499d40e09460db50171602192e87db645 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 10 Jul 2026 13:23:25 -0700 Subject: [PATCH 13/17] fix comment Signed-off-by: Alina (Xi) Li --- .../core/operator/expressions/array/map/test_map_as_errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py index bd71b0059..4950179c2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py @@ -1,7 +1,7 @@ """ Error tests for $map 'as' parameter. -Tests invalid 'as' types, invalid variable names, and reserved variable names. +Tests that $map rejects non-string 'as' values (including empty string). """ from datetime import datetime From 86951e5bdcb8002bdc244df0893c5d3629ab8ede Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 10 Jul 2026 13:26:56 -0700 Subject: [PATCH 14/17] inline applicable code Signed-off-by: Alina (Xi) Li --- .../core/operator/expressions/array/map/test_map_as_errors.py | 4 +--- .../operator/expressions/array/range/test_range_boundary.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py index 4950179c2..dd35ecb5d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_as_errors.py @@ -120,10 +120,8 @@ ), ] -ALL_AS_TESTS = INVALID_AS_TYPE_TESTS - -@pytest.mark.parametrize("test", pytest_params(ALL_AS_TESTS)) +@pytest.mark.parametrize("test", pytest_params(INVALID_AS_TYPE_TESTS)) def test_map_invalid_as(collection, test): """Test $map with invalid 'as' parameter values.""" result = execute_expression(collection, test.expression) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py index 32fe0916a..dcb815f3f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py @@ -78,10 +78,8 @@ ), ] -ALL_BOUNDARY_TESTS = INT32_BOUNDARY_TESTS - -@pytest.mark.parametrize("test", pytest_params(ALL_BOUNDARY_TESTS)) +@pytest.mark.parametrize("test", pytest_params(INT32_BOUNDARY_TESTS)) def test_range_int32_boundary(collection, test): """Test $range with INT32 boundary values for start, end, step.""" if test.doc is None: From b5e2bb52ccdd3386358412a8488581b5726f34d7 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 10 Jul 2026 13:38:33 -0700 Subject: [PATCH 15/17] add missing BSON types Signed-off-by: Alina (Xi) Li --- .../operator/expressions/array/map/test_map_errors.py | 9 ++++++++- .../operator/expressions/array/zip/test_zip_errors.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py index e82a6ba97..98440c698 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/map/test_map_errors.py @@ -9,7 +9,7 @@ from datetime import datetime, timezone import pytest -from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 ExpressionTestCase, @@ -133,6 +133,13 @@ error_code=MAP_INPUT_NOT_ARRAY_ERROR, msg="$map should reject regex input", ), + ExpressionTestCase( + "code_input", + expression={"$map": {"input": "$arr", "in": "$$this"}}, + doc={"arr": Code("function(){}")}, + error_code=MAP_INPUT_NOT_ARRAY_ERROR, + msg="$map should reject javascript code input", + ), ExpressionTestCase( "maxkey_input", expression={"$map": {"input": "$arr", "in": "$$this"}}, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py index 7190ddb8f..5889ab0e0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py @@ -8,7 +8,7 @@ from datetime import datetime, timezone import pytest -from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 ExpressionTestCase, @@ -136,6 +136,13 @@ error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, msg="$zip should reject regex input element", ), + ExpressionTestCase( + "code_input", + doc={"arr0": Code("function(){}"), "arr1": [1]}, + expression={"$zip": {"inputs": ["$arr0", "$arr1"]}}, + error_code=ZIP_REQUIRES_ARRAY_ELEMENT_ERROR, + msg="$zip should reject javascript code input element", + ), ExpressionTestCase( "maxkey_input", doc={"arr0": MaxKey(), "arr1": [1]}, From 9b13702b790608c86c194d1d14d9d879378aae4a Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 10 Jul 2026 13:41:45 -0700 Subject: [PATCH 16/17] fix comments Signed-off-by: Alina (Xi) Li --- .../expressions/array/range/test_range_boundary.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py index dcb815f3f..2428be5d1 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py @@ -1,7 +1,7 @@ """ Boundary value tests for $range expression. -Tests INT32 boundary values and negative zero handling for start, end, step. +Tests INT32 boundary value handling for start, end, step. """ import pytest @@ -24,28 +24,28 @@ doc={"start": INT32_MAX, "end": INT32_MAX}, expression={"$range": ["$start", "$end"]}, expected=[], - msg="$range iNT32_MAX == INT32_MAX should be empty", + msg="$range INT32_MAX == INT32_MAX should be empty", ), ExpressionTestCase( "int32_min_eq", doc={"start": INT32_MIN, "end": INT32_MIN}, expression={"$range": ["$start", "$end"]}, expected=[], - msg="$range iNT32_MIN == INT32_MIN should be empty", + msg="$range INT32_MIN == INT32_MIN should be empty", ), ExpressionTestCase( "int32_max_minus1", doc={"start": INT32_MAX_MINUS_1, "end": INT32_MAX}, expression={"$range": ["$start", "$end"]}, expected=[INT32_MAX_MINUS_1], - msg="$range iNT32_MAX-1 to INT32_MAX should produce single element", + msg="$range INT32_MAX-1 to INT32_MAX should produce single element", ), ExpressionTestCase( "int32_min_to_plus3", doc={"start": INT32_MIN, "end": INT32_MIN + 3}, expression={"$range": ["$start", "$end"]}, expected=[INT32_MIN, INT32_MIN + 1, INT32_MIN + 2], - msg="$range iNT32_MIN to INT32_MIN+3", + msg="$range INT32_MIN to INT32_MIN+3", ), ExpressionTestCase( "step_int32_max", From 6bb9f5cb81282f4bbd1a87fd52e680a781e7c3a9 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 10 Jul 2026 13:55:50 -0700 Subject: [PATCH 17/17] remove if test.doc check Signed-off-by: Alina (Xi) Li --- .../operator/expressions/array/range/test_range_boundary.py | 6 +----- .../operator/expressions/array/zip/test_zip_bson_types.py | 6 +----- .../expressions/array/zip/test_zip_degenerate_cases.py | 6 +----- .../core/operator/expressions/array/zip/test_zip_errors.py | 6 +----- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py index 2428be5d1..b50d89e2a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/range/test_range_boundary.py @@ -11,7 +11,6 @@ ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 assert_expression_result, - execute_expression, execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params @@ -82,10 +81,7 @@ @pytest.mark.parametrize("test", pytest_params(INT32_BOUNDARY_TESTS)) def test_range_int32_boundary(collection, test): """Test $range with INT32 boundary values for start, end, step.""" - if test.doc is None: - result = execute_expression(collection, test.expression) - else: - result = execute_expression_with_insert(collection, test.expression, test.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/zip/test_zip_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py index 4cd5c04c9..ff502fff7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_bson_types.py @@ -15,7 +15,6 @@ ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 assert_expression_result, - execute_expression, execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params @@ -390,10 +389,7 @@ @pytest.mark.parametrize("test", pytest_params(ALL_BSON_TESTS)) def test_zip_bson_type_preservation(collection, test): """Test $zip preserves BSON types in zipped output arrays.""" - if test.doc is None: - result = execute_expression(collection, test.expression) - else: - result = execute_expression_with_insert(collection, test.expression, test.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/zip/test_zip_degenerate_cases.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py index 5a8c63ba5..09e8faf6a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_degenerate_cases.py @@ -12,7 +12,6 @@ ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 assert_expression_result, - execute_expression, execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params @@ -234,10 +233,7 @@ @pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) def test_zip_edge_cases(collection, test): """Test $zip with empty, single, nested, null, large, and multi-length inputs.""" - if test.doc is None: - result = execute_expression(collection, test.expression) - else: - result = execute_expression_with_insert(collection, test.expression, test.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/zip/test_zip_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py index 5889ab0e0..5aa2a79b6 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/zip/test_zip_errors.py @@ -15,7 +15,6 @@ ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( # noqa: E501 assert_expression_result, - execute_expression, execute_expression_with_insert, ) from documentdb_tests.framework.error_codes import ( @@ -438,10 +437,7 @@ @pytest.mark.parametrize("test", pytest_params(ALL_INPUT_ELEMENT_TESTS)) def test_zip_non_array_input_error(collection, test): """Test $zip rejects non-array inputs and invalid useLongestLength/defaults.""" - if test.doc is None: - result = execute_expression(collection, test.expression) - else: - result = execute_expression_with_insert(collection, test.expression, test.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 )