From 4409e9ffa5c05f81894fad3a8239b9bd91e41206 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 30 Jun 2026 15:22:39 -0700 Subject: [PATCH 1/9] migrate arrayElemAt, arrayToObject, concatArrays Co-authored-by: Leszek Kurzyna Co-authored-by: Sean Oczkowski Signed-off-by: Alina (Xi) Li --- .../expressions/array/arrayElemAt/__init__.py | 0 ...st_expression_arrayElemAt_core_behavior.py | 213 ++++++++ ...st_expression_arrayElemAt_element_types.py | 194 ++++++++ .../test_expression_arrayElemAt_errors.py | 410 ++++++++++++++++ ...test_expression_arrayElemAt_expressions.py | 167 +++++++ ...est_expression_arrayElemAt_null_missing.py | 90 ++++ ...st_expression_arrayElemAt_numeric_index.py | 192 ++++++++ ...st_expression_arrayElemAt_out_of_bounds.py | 132 +++++ .../test_smoke_expression_arrayElemAt.py | 2 +- .../array/arrayElemAt/utils/__init__.py | 0 .../arrayElemAt/utils/arrayElemAt_common.py | 19 + .../array/arrayToObject/__init__.py | 0 ...est_expression_arrayToObject_bson_types.py | 461 ++++++++++++++++++ ..._expression_arrayToObject_core_behavior.py | 364 ++++++++++++++ .../test_expression_arrayToObject_errors.py | 436 +++++++++++++++++ ...st_expression_arrayToObject_expressions.py | 151 ++++++ .../test_smoke_expression_arrayToObject.py | 2 +- .../array/arrayToObject/utils/__init__.py | 0 .../utils/arrayToObject_common.py | 15 + .../array/concatArrays/__init__.py | 0 ...test_expression_concatArrays_bson_types.py | 229 +++++++++ ...t_expression_concatArrays_core_behavior.py | 319 ++++++++++++ .../test_expression_concatArrays_errors.py | 318 ++++++++++++ ...est_expression_concatArrays_expressions.py | 366 ++++++++++++++ .../test_smoke_expression_concatArrays.py | 2 +- .../array/concatArrays/utils/__init__.py | 0 .../concatArrays/utils/concatArrays_common.py | 15 + 27 files changed, 4094 insertions(+), 3 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_element_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_null_missing.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/arrayElemAt_common.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/arrayToObject_common.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/concatArrays_common.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py new file mode 100644 index 000000000..c6369bd2e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py @@ -0,0 +1,213 @@ +""" +Core behavior tests for $arrayElemAt expression. + +Tests basic positive/negative index access, duplicate values, and large arrays. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 + ArrayElemAtTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Success: basic positive index access +# --------------------------------------------------------------------------- +POSITIVE_INDEX_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="first_element", array=[1, 2, 3], idx=0, expected=1, msg="Should return first element" + ), + ArrayElemAtTest( + id="second_element", array=[1, 2, 3], idx=1, expected=2, msg="Should return second element" + ), + ArrayElemAtTest( + id="last_element", array=[1, 2, 3], idx=2, expected=3, msg="Should return last element" + ), + ArrayElemAtTest( + id="single_element_array", + array=[42], + idx=0, + expected=42, + msg="Should return single element", + ), + ArrayElemAtTest( + id="string_elements", + array=["a", "b", "c"], + idx=1, + expected="b", + msg="Should return string element", + ), + ArrayElemAtTest( + id="mixed_types", + array=[1, "two", 3.0, True], + idx=2, + expected=3.0, + msg="Should return element from mixed-type array", + ), + ArrayElemAtTest( + id="nested_array_element", + array=[[1, 2], [3, 4]], + idx=1, + expected=[3, 4], + msg="Should return nested array element", + ), + ArrayElemAtTest( + id="nested_object_element", + array=[{"a": 1}, {"b": 2}], + idx=0, + expected={"a": 1}, + msg="Should return nested object element", + ), + ArrayElemAtTest( + id="null_element_in_array", + array=[None, 1, 2], + idx=0, + expected=None, + msg="Should return null element", + ), + ArrayElemAtTest( + id="bool_element", + array=[True, False], + idx=1, + expected=False, + msg="Should return bool element", + ), +] + +# --------------------------------------------------------------------------- +# Success: negative index access +# --------------------------------------------------------------------------- +NEGATIVE_INDEX_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="last_via_neg1", + array=[1, 2, 3], + idx=-1, + expected=3, + msg="Should return last element via -1", + ), + ArrayElemAtTest( + id="second_to_last", array=[1, 2, 3], idx=-2, expected=2, msg="Should return second to last" + ), + ArrayElemAtTest( + id="first_via_neg_len", + array=[1, 2, 3], + idx=-3, + expected=1, + msg="Should return first via negative length", + ), + ArrayElemAtTest( + id="single_element_neg1", + array=[42], + idx=-1, + expected=42, + msg="Should return single element via -1", + ), +] + +# --------------------------------------------------------------------------- +# Success: duplicate values in array — index-based access unaffected +# --------------------------------------------------------------------------- +DUPLICATE_VALUE_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="dup_first", + array=[1, 1, 1], + idx=0, + expected=1, + msg="Duplicates don't affect index 0", + ), + ArrayElemAtTest( + id="dup_last", + array=[1, 1, 1], + idx=2, + expected=1, + msg="Duplicates don't affect last index", + ), + ArrayElemAtTest( + id="dup_neg", + array=["a", "a", "b", "a"], + idx=-1, + expected="a", + msg="Duplicates don't affect negative index", + ), +] + +# --------------------------------------------------------------------------- +# Success: large array +# --------------------------------------------------------------------------- +_LARGE_ARRAY_SIZE = 20_000 +_LARGE_ARRAY = list(range(_LARGE_ARRAY_SIZE)) + +LARGE_ARRAY_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="large_array_first", + array=_LARGE_ARRAY, + idx=0, + expected=0, + msg="Should return first in large array", + ), + ArrayElemAtTest( + id="large_array_last", + array=_LARGE_ARRAY, + idx=_LARGE_ARRAY_SIZE - 1, + expected=_LARGE_ARRAY_SIZE - 1, + msg="Should return last in large array", + ), + ArrayElemAtTest( + id="large_array_neg1", + array=_LARGE_ARRAY, + idx=-1, + expected=_LARGE_ARRAY_SIZE - 1, + msg="Should return last via -1 in large array", + ), + ArrayElemAtTest( + id="large_array_middle", + array=_LARGE_ARRAY, + idx=_LARGE_ARRAY_SIZE // 2, + expected=_LARGE_ARRAY_SIZE // 2, + msg="Should return middle in large array", + ), + ArrayElemAtTest( + id="large_array_neg_middle", + array=_LARGE_ARRAY, + idx=-(_LARGE_ARRAY_SIZE // 4), + expected=_LARGE_ARRAY_SIZE - _LARGE_ARRAY_SIZE // 4, + msg="Should return negative middle in large array", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = POSITIVE_INDEX_TESTS + NEGATIVE_INDEX_TESTS + DUPLICATE_VALUE_TESTS + LARGE_ARRAY_TESTS + +TEST_SUBSET_FOR_LITERAL = [ + POSITIVE_INDEX_TESTS[0], # first_element + NEGATIVE_INDEX_TESTS[0], # last_via_neg1 + LARGE_ARRAY_TESTS[0], # large_array_first +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_arrayElemAt_literal(collection, test): + """Test $arrayElemAt with literal values.""" + result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + 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_arrayElemAt_insert(collection, test): + """Test $arrayElemAt with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + ) + 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/arrayElemAt/test_expression_arrayElemAt_element_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_element_types.py new file mode 100644 index 000000000..3fb675bfc --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_element_types.py @@ -0,0 +1,194 @@ +""" +Element type preservation tests for $arrayElemAt expression. + +Tests that $arrayElemAt correctly returns elements of all BSON types +including special float/Decimal128 values and boundary integers. +""" + +import math +from datetime import datetime, timezone + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 + ArrayElemAtTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT64_MAX, +) + +# --------------------------------------------------------------------------- +# Success: various element BSON types stored in array +# --------------------------------------------------------------------------- +ELEMENT_TYPE_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="int64_element", + array=[Int64(99)], + idx=0, + expected=Int64(99), + msg="Should return Int64 element", + ), + ArrayElemAtTest( + id="decimal128_element", + array=[Decimal128("1.5")], + idx=0, + expected=Decimal128("1.5"), + msg="Should return Decimal128 element", + ), + ArrayElemAtTest( + id="datetime_element", + array=[datetime(2024, 1, 1, tzinfo=timezone.utc)], + idx=0, + expected=datetime(2024, 1, 1, tzinfo=timezone.utc), + msg="Should return datetime element", + ), + ArrayElemAtTest( + id="binary_element", + array=[Binary(b"\x01\x02", 0)], + idx=0, + expected=b"\x01\x02", + msg="Should return binary element", + ), + ArrayElemAtTest( + id="regex_element", + array=[Regex("^abc", "i")], + idx=0, + expected=Regex("^abc", "i"), + msg="Should return regex element", + ), + ArrayElemAtTest( + id="objectid_element", + array=[ObjectId("000000000000000000000001")], + idx=0, + expected=ObjectId("000000000000000000000001"), + msg="Should return ObjectId element", + ), + ArrayElemAtTest( + id="minkey_element", + array=[MinKey(), 1], + idx=0, + expected=MinKey(), + msg="Should return MinKey element", + ), + ArrayElemAtTest( + id="maxkey_element", + array=[1, MaxKey()], + idx=1, + expected=MaxKey(), + msg="Should return MaxKey element", + ), + ArrayElemAtTest( + id="timestamp_element", + array=[Timestamp(0, 0)], + idx=0, + expected=Timestamp(0, 0), + msg="Should return Timestamp element", + ), + # Special float values + ArrayElemAtTest( + id="float_nan_element", + array=[FLOAT_NAN, 1], + idx=0, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN element", + ), + ArrayElemAtTest( + id="float_infinity_element", + array=[FLOAT_INFINITY, 1], + idx=0, + expected=FLOAT_INFINITY, + msg="Should return Infinity element", + ), + ArrayElemAtTest( + id="float_neg_infinity_element", + array=[FLOAT_NEGATIVE_INFINITY, 1], + idx=0, + expected=FLOAT_NEGATIVE_INFINITY, + msg="Should return -Infinity element", + ), + # Special Decimal128 values + ArrayElemAtTest( + id="decimal128_nan_element", + array=[DECIMAL128_NAN, 1], + idx=0, + expected=DECIMAL128_NAN, + msg="Should return Decimal128 NaN element", + ), + ArrayElemAtTest( + id="decimal128_infinity_element", + array=[DECIMAL128_INFINITY, 1], + idx=0, + expected=DECIMAL128_INFINITY, + msg="Should return Decimal128 Infinity element", + ), + ArrayElemAtTest( + id="decimal128_neg_infinity_element", + array=[DECIMAL128_NEGATIVE_INFINITY, 1], + idx=0, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="Should return Decimal128 -Infinity element", + ), + # Boundary integer values as elements + ArrayElemAtTest( + id="int32_max_element", + array=[INT32_MAX, 0], + idx=0, + expected=INT32_MAX, + msg="Should return INT32_MAX element", + ), + ArrayElemAtTest( + id="int64_max_element", + array=[INT64_MAX, 0], + idx=0, + expected=INT64_MAX, + msg="Should return INT64_MAX element", + ), + # Mixed special values array + ArrayElemAtTest( + id="mixed_special_last", + array=[INT32_MAX, FLOAT_INFINITY, DECIMAL128_NAN], + idx=2, + expected=DECIMAL128_NAN, + msg="Should return element from mixed special values array", + ), +] + +TEST_SUBSET_FOR_LITERAL = [ + ELEMENT_TYPE_TESTS[0], # int64_element + ELEMENT_TYPE_TESTS[9], # float_nan_element + ELEMENT_TYPE_TESTS[-1], # mixed_special_last +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_arrayElemAt_literal(collection, test): + """Test $arrayElemAt element type preservation with literal values.""" + result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ELEMENT_TYPE_TESTS)) +def test_arrayElemAt_insert(collection, test): + """Test $arrayElemAt element type preservation with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + ) + 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/arrayElemAt/test_expression_arrayElemAt_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py new file mode 100644 index 000000000..2446376c9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py @@ -0,0 +1,410 @@ +""" +Error tests for $arrayElemAt expression. + +Tests non-array first argument, non-numeric index, non-integral numeric index, +and wrong arity errors. +""" + +from datetime import datetime + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 + ArrayElemAtTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + EXPRESSION_TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import INT64_MAX, INT64_MIN + +# --------------------------------------------------------------------------- +# Error: non-array first argument +# --------------------------------------------------------------------------- +ARRAY_TYPE_ERROR_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="string_as_array", + array="hello", + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject string as array", + ), + ArrayElemAtTest( + id="int_as_array", + array=42, + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject int as array", + ), + ArrayElemAtTest( + id="bool_true_as_array", + array=True, + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject bool true as array", + ), + ArrayElemAtTest( + id="bool_false_as_array", + array=False, + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject bool false as array", + ), + ArrayElemAtTest( + id="object_as_array", + array={"a": 1}, + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject object as array", + ), + ArrayElemAtTest( + id="double_as_array", + array=3.14, + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject double as array", + ), + ArrayElemAtTest( + id="decimal128_as_array", + array=Decimal128("1"), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject decimal128 as array", + ), + ArrayElemAtTest( + id="int64_as_array", + array=Int64(1), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject int64 as array", + ), + ArrayElemAtTest( + id="binary_as_array", + array=Binary(b"x", 0), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject binary as array", + ), + ArrayElemAtTest( + id="datetime_as_array", + array=datetime(2024, 1, 1), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject datetime as array", + ), + ArrayElemAtTest( + id="objectid_as_array", + array=ObjectId(), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject objectid as array", + ), + ArrayElemAtTest( + id="regex_as_array", + array=Regex("x"), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject regex as array", + ), + ArrayElemAtTest( + id="maxkey_as_array", + array=MaxKey(), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject maxkey as array", + ), + ArrayElemAtTest( + id="minkey_as_array", + array=MinKey(), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject minkey as array", + ), + ArrayElemAtTest( + id="timestamp_as_array", + array=Timestamp(0, 0), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject timestamp as array", + ), + ArrayElemAtTest( + id="nan_as_array", + array=float("nan"), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject NaN as array", + ), + ArrayElemAtTest( + id="inf_as_array", + array=float("inf"), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject Infinity as array", + ), + ArrayElemAtTest( + id="decimal128_nan_as_array", + array=Decimal128("NaN"), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject Decimal128 NaN as array", + ), + ArrayElemAtTest( + id="decimal128_inf_as_array", + array=Decimal128("Infinity"), + idx=0, + error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, + msg="Should reject Decimal128 Infinity as array", + ), +] + +# --------------------------------------------------------------------------- +# Error: non-numeric index +# --------------------------------------------------------------------------- +INDEX_TYPE_ERROR_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="string_index", + array=[1, 2], + idx="0", + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject string index", + ), + ArrayElemAtTest( + id="bool_true_index", + array=[1, 2], + idx=True, + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject bool true index", + ), + ArrayElemAtTest( + id="bool_false_index", + array=[1, 2], + idx=False, + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject bool false index", + ), + ArrayElemAtTest( + id="array_index", + array=[1, 2], + idx=[0], + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject array index", + ), + ArrayElemAtTest( + id="object_index", + array=[1, 2], + idx={"a": 0}, + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject object index", + ), + ArrayElemAtTest( + id="objectid_index", + array=[1, 2], + idx=ObjectId(), + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject objectid index", + ), + ArrayElemAtTest( + id="binary_index", + array=[1, 2], + idx=Binary(b"\x01", 0), + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject binary index", + ), + ArrayElemAtTest( + id="timestamp_index", + array=[1, 2], + idx=Timestamp(0, 0), + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject timestamp index", + ), + ArrayElemAtTest( + id="datetime_index", + array=[1, 2], + idx=datetime(2024, 1, 1), + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject datetime index", + ), + ArrayElemAtTest( + id="maxkey_index", + array=[1, 2], + idx=MaxKey(), + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject maxkey index", + ), + ArrayElemAtTest( + id="minkey_index", + array=[1, 2], + idx=MinKey(), + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject minkey index", + ), + ArrayElemAtTest( + id="regex_index", + array=[1, 2], + idx=Regex("x"), + error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, + msg="Should reject regex index", + ), +] + +# --------------------------------------------------------------------------- +# Error: non-integral numeric index +# --------------------------------------------------------------------------- +NON_INTEGRAL_INDEX_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="double_fractional_index", + array=[1, 2, 3], + idx=1.5, + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject fractional double index", + ), + ArrayElemAtTest( + id="decimal128_fractional_index", + array=[1, 2, 3], + idx=Decimal128("0.5"), + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject fractional decimal128 index", + ), + ArrayElemAtTest( + id="double_nan_index", + array=[1, 2, 3], + idx=float("nan"), + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject NaN index", + ), + ArrayElemAtTest( + id="double_inf_index", + array=[1, 2, 3], + idx=float("inf"), + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject infinity index", + ), + ArrayElemAtTest( + id="double_neg_inf_index", + array=[1, 2, 3], + idx=float("-inf"), + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject -infinity index", + ), + ArrayElemAtTest( + id="decimal128_nan_index", + array=[1, 2, 3], + idx=Decimal128("NaN"), + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject decimal128 NaN index", + ), + ArrayElemAtTest( + id="decimal128_inf_index", + array=[1, 2, 3], + idx=Decimal128("Infinity"), + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject decimal128 infinity index", + ), + ArrayElemAtTest( + id="decimal128_neg_inf_index", + array=[1, 2, 3], + idx=Decimal128("-Infinity"), + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject decimal128 -infinity index", + ), + ArrayElemAtTest( + id="int64_max_index", + array=[1, 2, 3], + idx=INT64_MAX, + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject INT64_MAX index", + ), + ArrayElemAtTest( + id="int64_min_index", + array=[1, 2, 3], + idx=INT64_MIN, + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject INT64_MIN index", + ), + ArrayElemAtTest( + id="large_double_index", + array=[1, 2, 3], + idx=1.0e18, + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject large double index", + ), + ArrayElemAtTest( + id="large_neg_double_index", + array=[1, 2, 3], + idx=-1.0e18, + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject large negative double index", + ), + ArrayElemAtTest( + id="decimal128_beyond_int32", + array=[1, 2, 3], + idx=Decimal128("2147483648"), + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject decimal128 beyond int32", + ), + ArrayElemAtTest( + id="decimal128_huge", + array=[1, 2, 3], + idx=Decimal128("9223372036854775808"), + error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, + msg="Should reject huge decimal128 index", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ARRAY_TYPE_ERROR_TESTS + INDEX_TYPE_ERROR_TESTS + NON_INTEGRAL_INDEX_TESTS + +TEST_SUBSET_FOR_LITERAL = [ + ARRAY_TYPE_ERROR_TESTS[0], # string_as_array + INDEX_TYPE_ERROR_TESTS[0], # string_index + NON_INTEGRAL_INDEX_TESTS[0], # double_fractional_index +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_arrayElemAt_literal(collection, test): + """Test $arrayElemAt error cases with literal values.""" + result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + 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_arrayElemAt_insert(collection, test): + """Test $arrayElemAt error cases with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + ) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +# --------------------------------------------------------------------------- +# Error: wrong arity +# --------------------------------------------------------------------------- +ARITY_ERROR_TESTS = [ + pytest.param({"$arrayElemAt": [[1, 2, 3]]}, id="one_arg"), + pytest.param({"$arrayElemAt": [[1, 2, 3], 0, 1]}, id="three_args"), + pytest.param({"$arrayElemAt": []}, id="zero_args"), + pytest.param({"$arrayElemAt": [[[1, 2, 3], 0]]}, id="nested_array_of_args"), +] + + +@pytest.mark.parametrize("expr", ARITY_ERROR_TESTS) +def test_arrayElemAt_syntax_error(collection, expr): + """Test $arrayElemAt errors with wrong number of arguments.""" + result = execute_expression(collection, expr) + assert_expression_result(result, error_code=EXPRESSION_TYPE_MISMATCH_ERROR) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py new file mode 100644 index 000000000..64e516954 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py @@ -0,0 +1,167 @@ +""" +Expression and field path tests for $arrayElemAt expression. + +Tests nested expressions, field path lookups, composite paths, +and path through array of objects. +""" + +import pytest +from bson import Decimal128 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.error_codes import ARRAY_ELEM_AT_INDEX_TYPE_ERROR + + +# --------------------------------------------------------------------------- +# Nested expressions +# --------------------------------------------------------------------------- +@pytest.mark.parametrize( + "expression,expected", + [ + # 2D array access: arr[1][0] + ({"$arrayElemAt": [{"$arrayElemAt": [[[10, 20], [30, 40]], 1]}, 0]}, 30), + # 3D array access: arr[1][0][1] + ( + { + "$arrayElemAt": [ + { + "$arrayElemAt": [ + {"$arrayElemAt": [[[[1, 2], [3, 4]], [[5, 6], [7, 8]]], 1]}, + 0, + ] + }, + 1, + ] + }, + 6, + ), + # 4D array access: arr[1][1][0][1] + ( + { + "$arrayElemAt": [ + { + "$arrayElemAt": [ + { + "$arrayElemAt": [ + { + "$arrayElemAt": [ + [ + [[[1, 2], [3, 4]], [[5, 6], [7, 8]]], + [[[9, 10], [11, 12]], [[13, 14], [15, 16]]], + ], + 1, + ] + }, + 1, + ] + }, + 0, + ] + }, + 1, + ] + }, + 14, + ), + ], + ids=["nested_2d_access", "nested_3d_access", "nested_4d_access"], +) +def test_arrayElemAt_nested_expression(collection, expression, expected): + """Test $arrayElemAt composed with other expressions.""" + result = execute_expression(collection, expression) + assert_expression_result(result, expected=expected) + + +# --------------------------------------------------------------------------- +# Field path lookups +# --------------------------------------------------------------------------- +@pytest.mark.parametrize( + "document,array_ref,idx,expected", + [ + ({"a": {"b": [10, 20, 30]}}, "$a.b", 1, 20), + ({"a": {"missing": 1}}, "$a.nonexistent", 0, None), + ({"a": {"b": {"c": [5, 6, 7]}}}, "$a.b.c", -1, 7), + ], + ids=["nested_field_path", "nonexistent_field_null", "deeply_nested_field"], +) +def test_arrayElemAt_field_lookup(collection, document, array_ref, idx, expected): + """Test $arrayElemAt with field path lookups from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": [array_ref, idx]}, document + ) + assert_expression_result(result, expected=expected) + + +# --------------------------------------------------------------------------- +# Field path: path through array of objects +# --------------------------------------------------------------------------- +def test_arrayElemAt_path_through_array_of_objects(collection): + """Test $arrayElemAt where field path traverses array of objects.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$a.b", 0]}, {"a": [{"b": 10}, {"b": 20}]} + ) + assert_expression_result(result, expected=10) + + +# --------------------------------------------------------------------------- +# Field path: composite path for index +# --------------------------------------------------------------------------- +def test_arrayElemAt_composite_path_for_index(collection): + """Test $arrayElemAt with nested field path as index.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": [[10, 20, 30], "$a.b"]}, {"a": {"b": 1}} + ) + assert_expression_result(result, expected=20) + + +def test_arrayElemAt_composite_array_as_array(collection): + """Test $arrayElemAt with composite array from $x.y as the array argument.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$x.y", 1]}, {"x": [{"y": 10}, {"y": 20}, {"y": 30}]} + ) + assert_expression_result(result, expected=20) + + +def test_arrayElemAt_composite_array_as_index(collection): + """Test $arrayElemAt rejects composite array from $x.y as the index argument.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": [[10, 20, 30], "$x.y"]}, {"x": [{"y": 0}, {"y": 1}]} + ) + assert_expression_result(result, error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR) + + +# --------------------------------------------------------------------------- +# Composite path with Decimal128 indices and OOB +# --------------------------------------------------------------------------- +@pytest.mark.parametrize( + "idx,expected", + [ + (Decimal128("0"), 1), + (Decimal128("-1"), 3), + ], + ids=["composite_decimal128_pos", "composite_decimal128_neg"], +) +def test_arrayElemAt_composite_path_decimal128(collection, idx, expected): + """Test $arrayElemAt with composite path $a.b and Decimal128 index.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$a.b", idx]}, {"a": [{"b": 1}, {"b": 2}, {"b": 3}]} + ) + assert_expression_result(result, expected=expected) + + +@pytest.mark.parametrize( + "idx", + [Decimal128("4"), Decimal128("-4")], + ids=["composite_decimal128_oob_pos", "composite_decimal128_oob_neg"], +) +def test_arrayElemAt_composite_path_decimal128_oob(collection, idx): + """Test $arrayElemAt with composite path $a.b and Decimal128 OOB index.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$a.b", idx]}, {"a": [{"b": 1}, {"b": 2}, {"b": 3}]} + ) + assertSuccess(result, [{}]) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_null_missing.py new file mode 100644 index 000000000..e40073a96 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_null_missing.py @@ -0,0 +1,90 @@ +""" +Null and missing field behavior tests for $arrayElemAt expression. + +Tests null propagation and missing field handling for array and index arguments. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 + ArrayElemAtTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import MISSING + +# --------------------------------------------------------------------------- +# Success: null array/index → null (runs both literal and insert) +# --------------------------------------------------------------------------- +NULL_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="null_array", array=None, idx=0, expected=None, msg="Should return null for null array" + ), + ArrayElemAtTest( + id="null_array_neg_idx", + array=None, + idx=-1, + expected=None, + msg="Should return null for null array with negative index", + ), + ArrayElemAtTest( + id="null_index", + array=[1, 2], + idx=None, + expected=None, + msg="Should return null for null index", + ), + ArrayElemAtTest( + id="both_null", array=None, idx=None, expected=None, msg="Should return null when both null" + ), +] + +# --------------------------------------------------------------------------- +# Success: missing field → null (literal only, MISSING is a field ref) +# --------------------------------------------------------------------------- +LITERAL_ONLY_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="missing_array", + array=MISSING, + idx=0, + expected=None, + msg="Should return null for missing array", + ), + ArrayElemAtTest( + id="missing_index", + array=[1, 2, 3], + idx=MISSING, + expected=None, + msg="Should return null for missing index", + ), +] + +TEST_SUBSET_FOR_LITERAL = [ + NULL_TESTS[0], # null_array + NULL_TESTS[2], # null_index + NULL_TESTS[3], # both_null +] + LITERAL_ONLY_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_arrayElemAt_literal(collection, test): + """Test $arrayElemAt null/missing with literal values.""" + result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(NULL_TESTS)) +def test_arrayElemAt_insert(collection, test): + """Test $arrayElemAt null with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + ) + 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/arrayElemAt/test_expression_arrayElemAt_numeric_index.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py new file mode 100644 index 000000000..1213c7e70 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py @@ -0,0 +1,192 @@ +""" +Numeric index type tests for $arrayElemAt expression. + +Tests various numeric types (Int64, double, Decimal128) and edge cases +like negative zero and Decimal128 scientific notation as index values. +""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 + ArrayElemAtTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import DECIMAL128_NEGATIVE_ZERO, DOUBLE_NEGATIVE_ZERO + +# --------------------------------------------------------------------------- +# Success: numeric index types (int, int64, double that is integral) +# --------------------------------------------------------------------------- +NUMERIC_INDEX_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="int64_zero_index", + array=[10, 20, 30], + idx=Int64(0), + expected=10, + msg="Should accept Int64 zero index", + ), + ArrayElemAtTest( + id="int64_index", + array=[10, 20, 30], + idx=Int64(1), + expected=20, + msg="Should accept Int64 index", + ), + ArrayElemAtTest( + id="double_integral_index", + array=[10, 20, 30], + idx=2.0, + expected=30, + msg="Should accept integral double index", + ), + ArrayElemAtTest( + id="decimal128_integral_index", + array=[10, 20, 30], + idx=Decimal128("0"), + expected=10, + msg="Should accept Decimal128 index", + ), + ArrayElemAtTest( + id="int64_negative_index", + array=[10, 20, 30], + idx=Int64(-1), + expected=30, + msg="Should accept negative Int64 index", + ), + ArrayElemAtTest( + id="double_negative_integral", + array=[10, 20, 30], + idx=-2.0, + expected=20, + msg="Should accept negative integral double index", + ), + ArrayElemAtTest( + id="negative_zero_index", + array=[10, 20, 30], + idx=-0.0, + expected=10, + msg="Should treat -0.0 as index 0", + ), + ArrayElemAtTest( + id="decimal128_negative_zero_index", + array=[10, 20, 30], + idx=DECIMAL128_NEGATIVE_ZERO, + expected=10, + msg="Should treat decimal128 -0 as index 0", + ), + ArrayElemAtTest( + id="double_negative_zero_const", + array=[10, 20, 30], + idx=DOUBLE_NEGATIVE_ZERO, + expected=10, + msg="Should treat double -0 const as index 0", + ), + ArrayElemAtTest( + id="decimal128_trailing_zero", + array=[10, 20, 30], + idx=Decimal128("1.0"), + expected=20, + msg="Should accept decimal128 with trailing zero", + ), + ArrayElemAtTest( + id="decimal128_subnormal_zero", + array=[10, 20, 30], + idx=Decimal128("0E-6176"), + expected=10, + msg="Should accept decimal128 subnormal zero", + ), + ArrayElemAtTest( + id="decimal128_20E_neg1", + array=[10, 20, 30], + idx=Decimal128("20E-1"), + expected=30, + msg="Should accept decimal128 20E-1 as index 2", + ), + ArrayElemAtTest( + id="decimal128_0_2E1", + array=[10, 20, 30], + idx=Decimal128("0.2E1"), + expected=30, + msg="Should accept decimal128 0.2E1 as index 2", + ), + ArrayElemAtTest( + id="decimal128_2E0", + array=[10, 20, 30], + idx=Decimal128("2E0"), + expected=30, + msg="Should accept decimal128 2E0 as index 2", + ), + ArrayElemAtTest( + id="decimal128_10E_neg1", + array=[10, 20, 30], + idx=Decimal128("10E-1"), + expected=20, + msg="Should accept decimal128 10E-1 as index 1", + ), + ArrayElemAtTest( + id="decimal128_negative_integral_index", + array=[10, 20, 30], + idx=Decimal128("-1"), + expected=30, + msg="Should accept negative Decimal128 integral index", + ), + ArrayElemAtTest( + id="decimal128_neg_10E_neg1", + array=[10, 20, 30], + idx=Decimal128("-10E-1"), + expected=30, + msg="Should accept decimal128 -10E-1 as index -1", + ), + ArrayElemAtTest( + id="decimal128_0E_pos3", + array=[10, 20, 30], + idx=Decimal128("0E+3"), + expected=10, + msg="Should accept decimal128 0E+3 as index 0", + ), + ArrayElemAtTest( + id="decimal128_0E_neg3", + array=[10, 20, 30], + idx=Decimal128("0E-3"), + expected=10, + msg="Should accept decimal128 0E-3 as index 0", + ), + ArrayElemAtTest( + id="decimal128_1_00000", + array=[10, 20, 30], + idx=Decimal128("1.00000"), + expected=20, + msg="Should accept decimal128 1.00000 as index 1", + ), +] + +TEST_SUBSET_FOR_LITERAL = [ + NUMERIC_INDEX_TESTS[0], # int64_index + NUMERIC_INDEX_TESTS[5], # negative_zero_index + NUMERIC_INDEX_TESTS[-1], # decimal128_1_00000 +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_arrayElemAt_literal(collection, test): + """Test $arrayElemAt numeric index types with literal values.""" + result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(NUMERIC_INDEX_TESTS)) +def test_arrayElemAt_insert(collection, test): + """Test $arrayElemAt numeric index types with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + ) + 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/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py new file mode 100644 index 000000000..4ed9b5c23 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py @@ -0,0 +1,132 @@ +""" +Out-of-bounds index tests for $arrayElemAt expression. + +Tests that $arrayElemAt returns no result (missing) when the index +exceeds array bounds in either direction. +""" + +import pytest +from bson import Decimal128 + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 + ArrayElemAtTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import INT32_MAX, INT32_MIN + +# --------------------------------------------------------------------------- +# Success: out-of-bounds index → no result (returns nothing / missing) +# --------------------------------------------------------------------------- +OUT_OF_BOUNDS_TESTS: list[ArrayElemAtTest] = [ + ArrayElemAtTest( + id="positive_oob", + array=[1, 2, 3], + idx=15, + expected=[{}], + msg="Should return no result for positive OOB", + ), + ArrayElemAtTest( + id="positive_oob_by_one", + array=[1, 2, 3], + idx=3, + expected=[{}], + msg="Should return no result for OOB by one", + ), + ArrayElemAtTest( + id="negative_oob", + array=[1, 2, 3], + idx=-4, + expected=[{}], + msg="Should return no result for negative OOB", + ), + ArrayElemAtTest( + id="negative_oob_large", + array=[1, 2, 3], + idx=-100, + expected=[{}], + msg="Should return no result for large negative OOB", + ), + ArrayElemAtTest( + id="empty_array_idx_0", + array=[], + idx=0, + expected=[{}], + msg="Should return no result for empty array idx 0", + ), + ArrayElemAtTest( + id="empty_array_neg1", + array=[], + idx=-1, + expected=[{}], + msg="Should return no result for empty array idx -1", + ), + ArrayElemAtTest( + id="int32_max_oob", + array=[1, 2, 3], + idx=INT32_MAX, + expected=[{}], + msg="Should return no result for INT32_MAX index", + ), + ArrayElemAtTest( + id="int32_min_oob", + array=[1, 2, 3], + idx=INT32_MIN, + expected=[{}], + msg="Should return no result for INT32_MIN index", + ), + ArrayElemAtTest( + id="single_element_oob_pos", + array=[42], + idx=1, + expected=[{}], + msg="Should return no result for single element OOB positive", + ), + ArrayElemAtTest( + id="single_element_oob_neg", + array=[42], + idx=-2, + expected=[{}], + msg="Should return no result for single element OOB negative", + ), + ArrayElemAtTest( + id="decimal128_oob_pos", + array=[1, 2, 3], + idx=Decimal128("15"), + expected=[{}], + msg="Should return no result for Decimal128 positive OOB", + ), + ArrayElemAtTest( + id="decimal128_oob_neg", + array=[1, 2, 3], + idx=Decimal128("-100"), + expected=[{}], + msg="Should return no result for Decimal128 negative OOB", + ), +] + +TEST_SUBSET_FOR_LITERAL = [ + OUT_OF_BOUNDS_TESTS[0], # positive_oob + OUT_OF_BOUNDS_TESTS[4], # empty_array_idx_0 + OUT_OF_BOUNDS_TESTS[-1], # decimal128_oob_neg +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_arrayElemAt_out_of_bounds_literal(collection, test): + """Test $arrayElemAt returns no result when index exceeds array bounds.""" + result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + assertSuccess(result, test.expected) + + +@pytest.mark.parametrize("test", pytest_params(OUT_OF_BOUNDS_TESTS)) +def test_arrayElemAt_out_of_bounds_insert(collection, test): + """Test $arrayElemAt out-of-bounds with inserted documents.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + ) + assertSuccess(result, test.expected) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_smoke_expression_arrayElemAt.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_smoke_expression_arrayElemAt.py index 1f287e081..ead213bb8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_smoke_expression_arrayElemAt.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_smoke_expression_arrayElemAt.py @@ -26,4 +26,4 @@ def test_smoke_expression_arrayElemAt(collection): ) expected = [{"_id": 1, "element": 20}, {"_id": 2, "element": 15}] - assertSuccess(result, expected, msg="Should support $arrayElemAt expression") + assertSuccess(result, expected, "Should support $arrayElemAt expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/arrayElemAt_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/arrayElemAt_common.py new file mode 100644 index 000000000..286445c37 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/arrayElemAt_common.py @@ -0,0 +1,19 @@ +""" +Shared test infrastructure for $arrayElemAt expression tests. + +Provides the ArrayElemAtTest dataclass and common imports used across +all $arrayElemAt test files. +""" + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class ArrayElemAtTest(BaseTestCase): + """Test case for $arrayElemAt operator.""" + + array: Any = None + idx: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py new file mode 100644 index 000000000..7321a28b9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py @@ -0,0 +1,461 @@ +""" +BSON type tests for $arrayToObject expression. + +Tests that various BSON value types are preserved when converting +arrays to objects, including special numeric values, boundary values, +UUID binary, nested BSON values, and numeric type equivalence, +across both k/v and pair input forms. +""" + +from datetime import datetime, timezone +from uuid import UUID + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayToObject.utils.arrayToObject_common import ( # noqa: E501 + ArrayToObjectTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertSuccess +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_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT64_MAX, + INT64_MIN, +) + +# --------------------------------------------------------------------------- +# BSON value types preserved (k/v form) +# --------------------------------------------------------------------------- +BSON_KV_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="kv_int64", + array=[{"k": "a", "v": Int64(99)}], + expected={"a": Int64(99)}, + msg="Should preserve Int64 value", + ), + ArrayToObjectTest( + id="kv_decimal128", + array=[{"k": "a", "v": Decimal128("3.14")}], + expected={"a": Decimal128("3.14")}, + msg="Should preserve Decimal128 value", + ), + ArrayToObjectTest( + id="kv_datetime", + array=[{"k": "a", "v": datetime(2024, 1, 1, tzinfo=timezone.utc)}], + expected={"a": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + msg="Should preserve datetime value", + ), + ArrayToObjectTest( + id="kv_objectid", + array=[{"k": "a", "v": ObjectId("000000000000000000000001")}], + expected={"a": ObjectId("000000000000000000000001")}, + msg="Should preserve ObjectId value", + ), + ArrayToObjectTest( + id="kv_bool_false", + array=[{"k": "a", "v": False}], + expected={"a": False}, + msg="Should preserve false value", + ), + ArrayToObjectTest( + id="kv_bool_true", + array=[{"k": "a", "v": True}], + expected={"a": True}, + msg="Should preserve true value", + ), + ArrayToObjectTest( + id="kv_null", + array=[{"k": "a", "v": None}], + expected={"a": None}, + msg="Should preserve null value", + ), + ArrayToObjectTest( + id="kv_regex", + array=[{"k": "a", "v": Regex("^abc", "i")}], + expected={"a": Regex("^abc", "i")}, + msg="Should preserve regex value", + ), + ArrayToObjectTest( + id="kv_minkey", + array=[{"k": "a", "v": MinKey()}], + expected={"a": MinKey()}, + msg="Should preserve MinKey value", + ), + ArrayToObjectTest( + id="kv_maxkey", + array=[{"k": "a", "v": MaxKey()}], + expected={"a": MaxKey()}, + msg="Should preserve MaxKey value", + ), + ArrayToObjectTest( + id="kv_binary", + array=[{"k": "a", "v": Binary(b"\x01\x02\x03", 0)}], + expected={"a": b"\x01\x02\x03"}, + msg="Should preserve Binary value", + ), + ArrayToObjectTest( + id="kv_timestamp", + array=[{"k": "a", "v": Timestamp(1234567890, 1)}], + expected={"a": Timestamp(1234567890, 1)}, + msg="Should preserve Timestamp value", + ), + ArrayToObjectTest( + id="kv_uuid", + array=[{"k": "a", "v": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))}], + expected={"a": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))}, + msg="Should preserve UUID binary value", + ), +] + +# --------------------------------------------------------------------------- +# BSON value types preserved (pair form) +# --------------------------------------------------------------------------- +BSON_PAIR_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="pair_int64", + array=[["a", Int64(99)]], + expected={"a": Int64(99)}, + msg="Should preserve Int64 value (pair form)", + ), + ArrayToObjectTest( + id="pair_decimal128", + array=[["a", Decimal128("3.14")]], + expected={"a": Decimal128("3.14")}, + msg="Should preserve Decimal128 value (pair form)", + ), + ArrayToObjectTest( + id="pair_datetime", + array=[["a", datetime(2024, 1, 1, tzinfo=timezone.utc)]], + expected={"a": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + msg="Should preserve datetime value (pair form)", + ), + ArrayToObjectTest( + id="pair_objectid", + array=[["a", ObjectId("000000000000000000000001")]], + expected={"a": ObjectId("000000000000000000000001")}, + msg="Should preserve ObjectId value (pair form)", + ), + ArrayToObjectTest( + id="pair_binary", + array=[["a", Binary(b"\x01\x02\x03", 0)]], + expected={"a": b"\x01\x02\x03"}, + msg="Should preserve Binary value (pair form)", + ), + ArrayToObjectTest( + id="pair_timestamp", + array=[["a", Timestamp(1234567890, 1)]], + expected={"a": Timestamp(1234567890, 1)}, + msg="Should preserve Timestamp value (pair form)", + ), + ArrayToObjectTest( + id="pair_regex", + array=[["a", Regex("^abc", "i")]], + expected={"a": Regex("^abc", "i")}, + msg="Should preserve regex value (pair form)", + ), + ArrayToObjectTest( + id="pair_minkey", + array=[["a", MinKey()]], + expected={"a": MinKey()}, + msg="Should preserve MinKey value (pair form)", + ), + ArrayToObjectTest( + id="pair_maxkey", + array=[["a", MaxKey()]], + expected={"a": MaxKey()}, + msg="Should preserve MaxKey value (pair form)", + ), + ArrayToObjectTest( + id="pair_uuid", + array=[["a", Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))]], + expected={"a": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))}, + msg="Should preserve UUID binary value (pair form)", + ), +] + +# --------------------------------------------------------------------------- +# Special numeric values +# --------------------------------------------------------------------------- +SPECIAL_NUMERIC_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="value_infinity", + array=[{"k": "a", "v": FLOAT_INFINITY}], + expected={"a": FLOAT_INFINITY}, + msg="Should preserve Infinity value", + ), + ArrayToObjectTest( + id="value_neg_infinity", + array=[{"k": "a", "v": FLOAT_NEGATIVE_INFINITY}], + expected={"a": FLOAT_NEGATIVE_INFINITY}, + msg="Should preserve -Infinity value", + ), + ArrayToObjectTest( + id="value_neg_zero", + array=[{"k": "a", "v": DOUBLE_NEGATIVE_ZERO}], + expected={"a": DOUBLE_NEGATIVE_ZERO}, + msg="Should preserve negative zero value", + ), + ArrayToObjectTest( + id="value_decimal128_nan", + array=[{"k": "a", "v": DECIMAL128_NAN}], + expected={"a": DECIMAL128_NAN}, + msg="Should preserve Decimal128 NaN value", + ), + ArrayToObjectTest( + id="value_decimal128_infinity", + array=[{"k": "a", "v": DECIMAL128_INFINITY}], + expected={"a": DECIMAL128_INFINITY}, + msg="Should preserve Decimal128 Infinity value", + ), + ArrayToObjectTest( + id="value_decimal128_neg_infinity", + array=[{"k": "a", "v": DECIMAL128_NEGATIVE_INFINITY}], + expected={"a": DECIMAL128_NEGATIVE_INFINITY}, + msg="Should preserve Decimal128 -Infinity value", + ), + ArrayToObjectTest( + id="value_decimal128_neg_zero", + array=[{"k": "a", "v": DECIMAL128_NEGATIVE_ZERO}], + expected={"a": DECIMAL128_NEGATIVE_ZERO}, + msg="Should preserve Decimal128 -0 value", + ), + ArrayToObjectTest( + id="value_decimal128_high_precision", + array=[{"k": "a", "v": Decimal128("1.234567890123456789012345678901234")}], + expected={"a": Decimal128("1.234567890123456789012345678901234")}, + msg="Should preserve full Decimal128 precision", + ), + ArrayToObjectTest( + id="value_decimal128_zero_exponent", + array=[{"k": "a", "v": Decimal128("0E+10")}], + expected={"a": Decimal128("0E+10")}, + msg="Should preserve Decimal128 exponent notation", + ), + ArrayToObjectTest( + id="value_decimal128_trailing_zeros", + array=[{"k": "a", "v": Decimal128("1.00000")}], + expected={"a": Decimal128("1.00000")}, + msg="Should preserve Decimal128 trailing zeros", + ), + ArrayToObjectTest( + id="value_decimal128_subnormal_zero", + array=[{"k": "a", "v": Decimal128("0E-6176")}], + expected={"a": Decimal128("0E-6176")}, + msg="Should preserve Decimal128 subnormal zero", + ), +] + +# --------------------------------------------------------------------------- +# Boundary values +# --------------------------------------------------------------------------- +BOUNDARY_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="value_int32_max", + array=[{"k": "a", "v": INT32_MAX}], + expected={"a": INT32_MAX}, + msg="Should preserve INT32_MAX value", + ), + ArrayToObjectTest( + id="value_int32_min", + array=[{"k": "a", "v": INT32_MIN}], + expected={"a": INT32_MIN}, + msg="Should preserve INT32_MIN value", + ), + ArrayToObjectTest( + id="value_int64_max", + array=[{"k": "a", "v": INT64_MAX}], + expected={"a": INT64_MAX}, + msg="Should preserve INT64_MAX value", + ), + ArrayToObjectTest( + id="value_int64_min", + array=[{"k": "a", "v": INT64_MIN}], + expected={"a": INT64_MIN}, + msg="Should preserve INT64_MIN value", + ), + ArrayToObjectTest( + id="value_decimal128_max", + array=[{"k": "a", "v": DECIMAL128_MAX}], + expected={"a": DECIMAL128_MAX}, + msg="Should preserve DECIMAL128_MAX value", + ), + ArrayToObjectTest( + id="value_decimal128_min", + array=[{"k": "a", "v": DECIMAL128_MIN}], + expected={"a": DECIMAL128_MIN}, + msg="Should preserve DECIMAL128_MIN value", + ), +] + +# --------------------------------------------------------------------------- +# Nested BSON values +# --------------------------------------------------------------------------- +NESTED_BSON_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="nested_bson_in_object_value", + array=[{"k": "a", "v": {"x": Int64(1), "y": Decimal128("2.5")}}], + expected={"a": {"x": Int64(1), "y": Decimal128("2.5")}}, + msg="Should preserve nested BSON types in object value", + ), + ArrayToObjectTest( + id="nested_bson_in_array_value", + array=[ + { + "k": "a", + "v": [ + MinKey(), + datetime(2024, 1, 1, tzinfo=timezone.utc), + ObjectId("000000000000000000000001"), + ], + } + ], + expected={ + "a": [ + MinKey(), + datetime(2024, 1, 1, tzinfo=timezone.utc), + ObjectId("000000000000000000000001"), + ] + }, + msg="Should preserve nested BSON types in array value", + ), + ArrayToObjectTest( + id="deeply_nested_bson", + array=[{"k": "a", "v": {"x": [{"y": Decimal128("1.5")}, Timestamp(0, 0)]}}], + expected={"a": {"x": [{"y": Decimal128("1.5")}, Timestamp(0, 0)]}}, + msg="Should preserve deeply nested BSON types", + ), + ArrayToObjectTest( + id="nested_array_not_interpreted_as_kv", + array=[{"k": "a", "v": [["level2", {"x": 1}]]}], + expected={"a": [["level2", {"x": 1}]]}, + msg="Should preserve nested array as value without interpreting as k/v", + ), +] + +# --------------------------------------------------------------------------- +# Numeric type equivalence — duplicate keys with different numeric types +# --------------------------------------------------------------------------- +NUMERIC_EQUIVALENCE_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="duplicate_key_int_then_int64", + array=[{"k": "a", "v": 1}, {"k": "a", "v": Int64(2)}], + expected={"a": Int64(2)}, + msg="Int64 value should win as last duplicate key", + ), + ArrayToObjectTest( + id="duplicate_key_int_then_decimal128", + array=[{"k": "a", "v": 1}, {"k": "a", "v": Decimal128("2")}], + expected={"a": Decimal128("2")}, + msg="Decimal128 value should win as last duplicate key", + ), + ArrayToObjectTest( + id="duplicate_key_decimal128_then_double", + array=[{"k": "a", "v": Decimal128("1")}, {"k": "a", "v": 2.0}], + expected={"a": 2.0}, + msg="Double value should win as last duplicate key", + ), +] + +# --------------------------------------------------------------------------- +# Mixed BSON types in a single array +# --------------------------------------------------------------------------- +MIXED_BSON_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="kv_mixed_bson_types", + array=[ + {"k": "int64", "v": Int64(1)}, + {"k": "dec", "v": Decimal128("1.5")}, + {"k": "dt", "v": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + {"k": "oid", "v": ObjectId("000000000000000000000001")}, + {"k": "bin", "v": Binary(b"\x01", 0)}, + {"k": "ts", "v": Timestamp(0, 0)}, + {"k": "min", "v": MinKey()}, + ], + expected={ + "int64": Int64(1), + "dec": Decimal128("1.5"), + "dt": datetime(2024, 1, 1, tzinfo=timezone.utc), + "oid": ObjectId("000000000000000000000001"), + "bin": b"\x01", + "ts": Timestamp(0, 0), + "min": MinKey(), + }, + msg="Should preserve multiple mixed BSON types in one conversion", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_BSON_TESTS = ( + BSON_KV_TESTS + + BSON_PAIR_TESTS + + SPECIAL_NUMERIC_TESTS + + BOUNDARY_TESTS + + NESTED_BSON_TESTS + + NUMERIC_EQUIVALENCE_TESTS + + MIXED_BSON_TESTS +) + +TEST_SUBSET_FOR_LITERAL = [ + BSON_KV_TESTS[0], # kv_int64 + BSON_KV_TESTS[10], # kv_binary + BSON_KV_TESTS[12], # kv_uuid + BSON_PAIR_TESTS[0], # pair_int64 + SPECIAL_NUMERIC_TESTS[0], # value_infinity + BOUNDARY_TESTS[0], # value_int32_max + NESTED_BSON_TESTS[0], # nested_bson_in_object_value + MIXED_BSON_TESTS[0], # kv_mixed_bson_types +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_arrayToObject_bson_literal(collection, test): + """Test $arrayToObject BSON types with literal values.""" + result = execute_expression(collection, {"$arrayToObject": {"$literal": test.array}}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ALL_BSON_TESTS)) +def test_arrayToObject_bson_insert(collection, test): + """Test $arrayToObject BSON types with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$arrayToObject": "$arr"}, {"arr": test.array} + ) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +# --------------------------------------------------------------------------- +# Float NaN — requires special handling since nan != nan +# --------------------------------------------------------------------------- +def test_arrayToObject_float_nan_value(collection): + """Test $arrayToObject preserves float NaN value.""" + import math + + result = execute_expression(collection, {"$arrayToObject": {"$literal": [["a", float("nan")]]}}) + assertSuccess( + result, + expected=[{"result": True}], + transform=lambda batch: [{"result": math.isnan(batch[0]["result"]["a"])}], + msg="NaN value should be preserved", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py new file mode 100644 index 000000000..bdba332ad --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py @@ -0,0 +1,364 @@ +""" +Core behavior tests for $arrayToObject expression. + +Tests both input forms (k/v documents and two-element arrays), empty arrays, +duplicate keys, format equivalence, field ordering, case sensitivity, +value edge cases, key edge cases, and large inputs. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayToObject.utils.arrayToObject_common import ( # noqa: E501 + ArrayToObjectTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Success: k/v document form — [{k: , v: }, ...] +# --------------------------------------------------------------------------- +KV_FORM_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="kv_single_pair", + array=[{"k": "a", "v": 1}], + expected={"a": 1}, + msg="Should convert single k/v pair", + ), + ArrayToObjectTest( + id="kv_multiple_pairs", + array=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "c", "v": 3}], + expected={"a": 1, "b": 2, "c": 3}, + msg="Should convert multiple k/v pairs", + ), + ArrayToObjectTest( + id="kv_string_values", + array=[{"k": "name", "v": "Alice"}, {"k": "city", "v": "Mycity"}], + expected={"name": "Alice", "city": "Mycity"}, + msg="Should convert k/v pairs with string values", + ), + ArrayToObjectTest( + id="kv_mixed_value_types", + array=[ + {"k": "int", "v": 1}, + {"k": "str", "v": "hello"}, + {"k": "bool", "v": True}, + {"k": "null", "v": None}, + ], + expected={"int": 1, "str": "hello", "bool": True, "null": None}, + msg="Should convert k/v pairs with mixed value types", + ), + ArrayToObjectTest( + id="kv_nested_object_value", + array=[{"k": "obj", "v": {"x": 1, "y": 2}}], + expected={"obj": {"x": 1, "y": 2}}, + msg="Should convert k/v pair with nested object value", + ), + ArrayToObjectTest( + id="kv_array_value", + array=[{"k": "arr", "v": [1, 2, 3]}], + expected={"arr": [1, 2, 3]}, + msg="Should convert k/v pair with array value", + ), +] + +# --------------------------------------------------------------------------- +# Success: two-element array form — [[, ], ...] +# --------------------------------------------------------------------------- +TWO_ELEM_FORM_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="pair_single", + array=[["a", 1]], + expected={"a": 1}, + msg="Should convert single two-element pair", + ), + ArrayToObjectTest( + id="pair_multiple", + array=[["a", 1], ["b", 2], ["c", 3]], + expected={"a": 1, "b": 2, "c": 3}, + msg="Should convert multiple two-element pairs", + ), + ArrayToObjectTest( + id="pair_string_values", + array=[["name", "Alice"], ["city", "Mycity"]], + expected={"name": "Alice", "city": "Mycity"}, + msg="Should convert pairs with string values", + ), + ArrayToObjectTest( + id="pair_mixed_value_types", + array=[["int", 1], ["str", "hello"], ["bool", True], ["null", None]], + expected={"int": 1, "str": "hello", "bool": True, "null": None}, + msg="Should convert pairs with mixed value types", + ), + ArrayToObjectTest( + id="pair_nested_object_value", + array=[["obj", {"x": 1, "y": 2}]], + expected={"obj": {"x": 1, "y": 2}}, + msg="Should convert pair with nested object value", + ), + ArrayToObjectTest( + id="pair_array_value", + array=[["arr", [1, 2, 3]]], + expected={"arr": [1, 2, 3]}, + msg="Should convert pair with array value", + ), +] + +# --------------------------------------------------------------------------- +# Success: empty or null +# --------------------------------------------------------------------------- +EMPTY_AND_NULL_ARRAY_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="empty_array", + array=[], + expected={}, + msg="Should return empty object for empty array", + ), + ArrayToObjectTest( + id="null_array", + array=None, + expected=None, + msg="Should return null for null array", + ), +] + +# --------------------------------------------------------------------------- +# Success: duplicate keys — last value wins +# --------------------------------------------------------------------------- +DUPLICATE_KEY_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="kv_duplicate_keys", + array=[{"k": "a", "v": 1}, {"k": "a", "v": 2}], + expected={"a": 2}, + msg="Last value should win for duplicate keys (k/v form)", + ), + ArrayToObjectTest( + id="pair_duplicate_keys", + array=[["a", 1], ["a", 2]], + expected={"a": 2}, + msg="Last value should win for duplicate keys (pair form)", + ), + ArrayToObjectTest( + id="kv_triple_duplicate", + array=[{"k": "x", "v": 1}, {"k": "x", "v": 2}, {"k": "x", "v": 3}], + expected={"x": 3}, + msg="Last of three duplicate keys should win", + ), + ArrayToObjectTest( + id="pair_dup_different_types", + array=[["a", 1], ["a", "hello"]], + expected={"a": "hello"}, + msg="Last value should win even with different value types", + ), + ArrayToObjectTest( + id="pair_dup_interspersed", + array=[["a", 1], ["b", 2], ["a", 3]], + expected={"a": 3, "b": 2}, + msg="Last value should win with interspersed duplicate keys", + ), + ArrayToObjectTest( + id="kv_dup_interspersed", + array=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "a", "v": 3}], + expected={"a": 3, "b": 2}, + msg="Last value should win with interspersed duplicates (k/v form)", + ), + ArrayToObjectTest( + id="kv_reversed_field_order", + array=[{"v": "val", "k": "key"}], + expected={"key": "val"}, + msg="Should work regardless of k/v field order in document", + ), +] + +# --------------------------------------------------------------------------- +# Success: key edge cases (unicode, emoji, spaces) +# --------------------------------------------------------------------------- +KEY_EDGE_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="unicode_key", + array=[{"k": "日本語", "v": 1}], + expected={"日本語": 1}, + msg="Unicode key should be valid", + ), + ArrayToObjectTest( + id="emoji_key", + array=[{"k": "🔑", "v": "value"}], + expected={"🔑": "value"}, + msg="Emoji key should be valid", + ), + ArrayToObjectTest( + id="key_with_spaces", + array=[["key with spaces", 1]], + expected={"key with spaces": 1}, + msg="Key with spaces should be valid", + ), + ArrayToObjectTest( + id="numeric_string_keys", + array=[["0", "a"], ["1", "b"]], + expected={"0": "a", "1": "b"}, + msg="Numeric string keys should be treated as strings", + ), + ArrayToObjectTest( + id="underscore_id_key", + array=[["_id", 1]], + expected={"_id": 1}, + msg="_id key should be valid", + ), + ArrayToObjectTest( + id="operator_like_key", + array=[["$set", 1]], + expected={"$set": 1}, + msg="Operator-like key should be valid", + ), + ArrayToObjectTest( + id="very_long_key", + array=[["k" * 1024, 1]], + expected={"k" * 1024: 1}, + msg="Very long key should not be truncated", + ), +] + +# --------------------------------------------------------------------------- +# Success: field ordering, case sensitivity, value/key edge cases +# --------------------------------------------------------------------------- +EDGE_CASE_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="output_field_order", + array=[["z", 1], ["a", 2], ["m", 3]], + expected={"z": 1, "a": 2, "m": 3}, + msg="Output field order should match input order", + ), + ArrayToObjectTest( + id="case_sensitive_keys_kv", + array=[{"k": "price", "v": 24}, {"k": "PRICE", "v": 100}], + expected={"price": 24, "PRICE": 100}, + msg="Case-differing keys should be distinct", + ), + ArrayToObjectTest( + id="case_sensitive_keys_pair", + array=[["price", 24], ["PRICE", 100]], + expected={"price": 24, "PRICE": 100}, + msg="Case-differing keys should be distinct (pair form)", + ), + ArrayToObjectTest( + id="deeply_nested_object_value", + array=[["key", {"a": {"b": {"c": {"d": 1}}}}]], + expected={"key": {"a": {"b": {"c": {"d": 1}}}}}, + msg="Should handle deeply nested object", + ), + ArrayToObjectTest( + id="deeply_nested_array_value", + array=[["key", [[[[1]]]]]], + expected={"key": [[[[1]]]]}, + msg="Should handle deeply nested array", + ), + ArrayToObjectTest( + id="empty_object_value", + array=[["key", {}]], + expected={"key": {}}, + msg="Should handle empty object value", + ), + ArrayToObjectTest( + id="empty_array_value", + array=[["key", []]], + expected={"key": []}, + msg="Should handle empty array value", + ), + ArrayToObjectTest( + id="empty_string_value", + array=[["key", ""]], + expected={"key": ""}, + msg="Should handle empty string value", + ), + ArrayToObjectTest( + id="large_string_value", + array=[["key", "x" * 10240]], + expected={"key": "x" * 10240}, + msg="Should handle large string value", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + KV_FORM_TESTS + + TWO_ELEM_FORM_TESTS + + EMPTY_AND_NULL_ARRAY_TESTS + + DUPLICATE_KEY_TESTS + + KEY_EDGE_TESTS + + EDGE_CASE_TESTS +) + +TEST_SUBSET_FOR_LITERAL = [ + KV_FORM_TESTS[0], # kv_single_pair + KV_FORM_TESTS[3], # kv_mixed_value_types + TWO_ELEM_FORM_TESTS[0], # pair_single + EMPTY_AND_NULL_ARRAY_TESTS[0], # empty_array + DUPLICATE_KEY_TESTS[0], # kv_duplicate_keys + EDGE_CASE_TESTS[0], # output_field_order +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_arrayToObject_literal(collection, test): + """Test $arrayToObject with literal values.""" + result = execute_expression(collection, {"$arrayToObject": {"$literal": test.array}}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_arrayToObject_insert(collection, test): + """Test $arrayToObject with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$arrayToObject": "$arr"}, {"arr": test.array} + ) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +# --------------------------------------------------------------------------- +# Large inputs — standalone because assertSuccess+transform is used to +# spot-check a few fields instead of comparing the full 10K-field object. +# --------------------------------------------------------------------------- +def test_arrayToObject_large_array_two_element(collection): + """Test $arrayToObject with 10,000 two-element pairs.""" + large_arr = [[f"key_{i}", i] for i in range(10000)] + result = execute_expression(collection, {"$arrayToObject": {"$literal": large_arr}}) + assertSuccess( + result, + expected=[{"has_10k": True, "first": 0, "last": 9999}], + transform=lambda batch: [ + { + "has_10k": len(batch[0]["result"]) == 10000, + "first": batch[0]["result"]["key_0"], + "last": batch[0]["result"]["key_9999"], + } + ], + msg="Should produce 10,000 fields with correct values", + ) + + +def test_arrayToObject_large_array_kv(collection): + """Test $arrayToObject with 10,000 k/v documents.""" + large_arr = [{"k": f"key_{i}", "v": i} for i in range(10000)] + result = execute_expression(collection, {"$arrayToObject": {"$literal": large_arr}}) + assertSuccess( + result, + expected=[{"has_10k": True, "first": 0, "last": 9999}], + transform=lambda batch: [ + { + "has_10k": len(batch[0]["result"]) == 10000, + "first": batch[0]["result"]["key_0"], + "last": batch[0]["result"]["key_9999"], + } + ], + msg="Should produce 10,000 fields with correct values in k/v format", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py new file mode 100644 index 000000000..39e344c98 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py @@ -0,0 +1,436 @@ +""" +Error tests for $arrayToObject expression. + +Tests non-array input, invalid element format, non-string keys, +and wrong arity errors. +""" + +from datetime import datetime + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayToObject.utils.arrayToObject_common import ( # noqa: E501 + ArrayToObjectTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, + ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, + ARRAY_TO_OBJECT_INVALID_PAIR_ERROR, + ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, + ARRAY_TO_OBJECT_MIXED_KV_THEN_PAIR_ERROR, + ARRAY_TO_OBJECT_MIXED_PAIR_THEN_KV_ERROR, + ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + ARRAY_TO_OBJECT_NULL_BYTE_KV_KEY_ERROR, + ARRAY_TO_OBJECT_NULL_BYTE_PAIR_KEY_ERROR, + ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, + ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, + EXPRESSION_TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Error: input not an array +# --------------------------------------------------------------------------- +NOT_ARRAY_ERROR_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="string_input", + array="hello", + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject string input", + ), + ArrayToObjectTest( + id="int_input", + array=42, + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject int input", + ), + ArrayToObjectTest( + id="bool_input", + array=True, + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject bool input", + ), + ArrayToObjectTest( + id="object_input", + array={"a": 1}, + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject object input", + ), + ArrayToObjectTest( + id="double_input", + array=3.14, + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject double input", + ), + ArrayToObjectTest( + id="decimal128_input", + array=Decimal128("1"), + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject decimal128 input", + ), + ArrayToObjectTest( + id="int64_input", + array=Int64(1), + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject int64 input", + ), + ArrayToObjectTest( + id="objectid_input", + array=ObjectId(), + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject objectid input", + ), + ArrayToObjectTest( + id="datetime_input", + array=datetime(2024, 1, 1), + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject datetime input", + ), + ArrayToObjectTest( + id="binary_input", + array=Binary(b"x", 0), + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject binary input", + ), + ArrayToObjectTest( + id="regex_input", + array=Regex("x"), + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject regex input", + ), + ArrayToObjectTest( + id="maxkey_input", + array=MaxKey(), + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject maxkey input", + ), + ArrayToObjectTest( + id="minkey_input", + array=MinKey(), + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject minkey input", + ), + ArrayToObjectTest( + id="timestamp_input", + array=Timestamp(0, 0), + error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, + msg="Should reject timestamp input", + ), +] + +# --------------------------------------------------------------------------- +# Error: invalid element format (not k/v doc or 2-element array) +# --------------------------------------------------------------------------- +INVALID_ELEMENT_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="element_is_string", + array=["not_a_pair"], + error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, + msg="Should reject string element", + ), + ArrayToObjectTest( + id="element_is_int", + array=[42], + error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, + msg="Should reject int element", + ), + ArrayToObjectTest( + id="element_is_null", + array=[None], + error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, + msg="Should reject null element", + ), + ArrayToObjectTest( + id="element_is_bool", + array=[True], + error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, + msg="Should reject bool element", + ), + ArrayToObjectTest( + id="element_is_double", + array=[3.14], + error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, + msg="Should reject double element", + ), + ArrayToObjectTest( + id="element_is_objectid", + array=[ObjectId()], + error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, + msg="Should reject ObjectId element", + ), + ArrayToObjectTest( + id="kv_missing_v", + array=[{"k": "a"}], + error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, + msg="Should reject k/v doc missing v field", + ), + ArrayToObjectTest( + id="kv_missing_k", + array=[{"v": 1}], + error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, + msg="Should reject k/v doc missing k field", + ), + ArrayToObjectTest( + id="kv_extra_field", + array=[{"k": "a", "v": 1, "extra": 2}], + error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, + msg="Should reject k/v doc with extra field", + ), + ArrayToObjectTest( + id="kv_empty_doc", + array=[{}], + error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, + msg="Should reject empty document", + ), + ArrayToObjectTest( + id="kv_wrong_field_names", + array=[{"y": "x", "x": "y"}], + error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, + msg="Should reject wrong field names", + ), + ArrayToObjectTest( + id="kv_uppercase_K", + array=[{"K": "k1", "v": 2}], + error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, + msg="Should reject uppercase K (case-sensitive)", + ), + ArrayToObjectTest( + id="kv_uppercase_V", + array=[{"k": "k1", "V": 2}], + error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, + msg="Should reject uppercase V (case-sensitive)", + ), + ArrayToObjectTest( + id="kv_key_value_names", + array=[{"key": "k1", "value": "v1"}], + error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, + msg="Should reject 'key'/'value' instead of 'k'/'v'", + ), + ArrayToObjectTest( + id="mix_valid_pair_and_invalid", + array=[["a", 1], 123], + error_code=ARRAY_TO_OBJECT_MIXED_PAIR_THEN_KV_ERROR, + msg="Should reject mix of valid pair and invalid element", + ), + ArrayToObjectTest( + id="pair_one_element", + array=[["a"]], + error_code=ARRAY_TO_OBJECT_INVALID_PAIR_ERROR, + msg="Should reject one-element array pair", + ), + ArrayToObjectTest( + id="pair_three_elements", + array=[["a", 1, 2]], + error_code=ARRAY_TO_OBJECT_INVALID_PAIR_ERROR, + msg="Should reject three-element array pair", + ), + ArrayToObjectTest( + id="pair_empty_array", + array=[[]], + error_code=ARRAY_TO_OBJECT_INVALID_PAIR_ERROR, + msg="Should reject empty array pair", + ), +] + +# --------------------------------------------------------------------------- +# Error: key not a string +# --------------------------------------------------------------------------- +KEY_NOT_STRING_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="kv_int_key", + array=[{"k": 1, "v": "val"}], + error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, + msg="Should reject int key in k/v form", + ), + ArrayToObjectTest( + id="kv_bool_key", + array=[{"k": True, "v": "val"}], + error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, + msg="Should reject bool key in k/v form", + ), + ArrayToObjectTest( + id="kv_null_key", + array=[{"k": None, "v": "val"}], + error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, + msg="Should reject null key in k/v form", + ), + ArrayToObjectTest( + id="kv_array_key", + array=[{"k": [1], "v": "val"}], + error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, + msg="Should reject array key in k/v form", + ), + ArrayToObjectTest( + id="kv_object_key", + array=[{"k": {"x": 1}, "v": "val"}], + error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, + msg="Should reject object key in k/v form", + ), + ArrayToObjectTest( + id="kv_double_key", + array=[{"k": 1.5, "v": "val"}], + error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, + msg="Should reject double key in k/v form", + ), + ArrayToObjectTest( + id="kv_int64_key", + array=[{"k": Int64(1), "v": "val"}], + error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, + msg="Should reject Int64 key in k/v form", + ), + ArrayToObjectTest( + id="kv_decimal128_key", + array=[{"k": Decimal128("1"), "v": "val"}], + error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, + msg="Should reject Decimal128 key in k/v form", + ), + ArrayToObjectTest( + id="pair_int_key", + array=[[1, "val"]], + error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, + msg="Should reject int key in pair form", + ), + ArrayToObjectTest( + id="pair_bool_key", + array=[[True, "val"]], + error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, + msg="Should reject bool key in pair form", + ), + ArrayToObjectTest( + id="pair_null_key", + array=[[None, "val"]], + error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, + msg="Should reject null key in pair form", + ), + ArrayToObjectTest( + id="pair_array_key", + array=[[[1], "val"]], + error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, + msg="Should reject array key in pair form", + ), + ArrayToObjectTest( + id="pair_object_key", + array=[[{"x": 1}, "val"]], + error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, + msg="Should reject object key in pair form", + ), + ArrayToObjectTest( + id="pair_double_key", + array=[[1.5, "val"]], + error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, + msg="Should reject double key in pair form", + ), + ArrayToObjectTest( + id="pair_int64_key", + array=[[Int64(1), "val"]], + error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, + msg="Should reject Int64 key in pair form", + ), + ArrayToObjectTest( + id="pair_decimal128_key", + array=[[Decimal128("1"), "val"]], + error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, + msg="Should reject Decimal128 key in pair form", + ), +] + +# --------------------------------------------------------------------------- +# Error: mixed formats (k/v then pair, pair then k/v) +# --------------------------------------------------------------------------- +MIXED_FORMAT_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="mixed_kv_then_pair", + array=[{"k": "price", "v": 24}, ["item", "apple"]], + error_code=ARRAY_TO_OBJECT_MIXED_KV_THEN_PAIR_ERROR, + msg="k/v doc followed by pair should fail", + ), + ArrayToObjectTest( + id="mixed_pair_then_kv", + array=[["item", "apple"], {"k": "price", "v": 24}], + error_code=ARRAY_TO_OBJECT_MIXED_PAIR_THEN_KV_ERROR, + msg="Pair followed by k/v doc should fail", + ), +] + +# --------------------------------------------------------------------------- +# Error: null byte in key +# --------------------------------------------------------------------------- +NULL_BYTE_KEY_TESTS: list[ArrayToObjectTest] = [ + ArrayToObjectTest( + id="null_byte_in_key_pair", + array=[["a\x00b", "value"]], + error_code=ARRAY_TO_OBJECT_NULL_BYTE_PAIR_KEY_ERROR, + msg="Null byte in key should fail (pair form)", + ), + ArrayToObjectTest( + id="null_byte_in_key_kv", + array=[{"k": "a\x00b", "v": "value"}], + error_code=ARRAY_TO_OBJECT_NULL_BYTE_KV_KEY_ERROR, + msg="Null byte in key should fail (k/v form)", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + NOT_ARRAY_ERROR_TESTS + + INVALID_ELEMENT_TESTS + + KEY_NOT_STRING_TESTS + + MIXED_FORMAT_TESTS + + NULL_BYTE_KEY_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_arrayToObject_insert(collection, test): + """Test $arrayToObject error cases with values from inserted documents.""" + result = execute_expression_with_insert( + collection, {"$arrayToObject": "$arr"}, {"arr": test.array} + ) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +TEST_SUBSET_FOR_LITERAL = [ + NOT_ARRAY_ERROR_TESTS[0], # string_input + NOT_ARRAY_ERROR_TESTS[3], # object_input + INVALID_ELEMENT_TESTS[0], # element_is_string + INVALID_ELEMENT_TESTS[6], # kv_missing_v + KEY_NOT_STRING_TESTS[0], # kv_int_key + KEY_NOT_STRING_TESTS[8], # pair_int_key + MIXED_FORMAT_TESTS[0], # mixed_kv_then_pair +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_arrayToObject_literal(collection, test): + """Test $arrayToObject error cases with literal values.""" + # Use $literal for array inputs to prevent MongoDB from interpreting them as arguments + expr = {"$literal": test.array} if isinstance(test.array, list) else test.array + result = execute_expression(collection, {"$arrayToObject": expr}) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +# --------------------------------------------------------------------------- +# Error: wrong arity +# --------------------------------------------------------------------------- +ARITY_ERROR_TESTS = [ + pytest.param({"$arrayToObject": [[], []]}, id="two_args"), +] + + +@pytest.mark.parametrize("expr", ARITY_ERROR_TESTS) +def test_arrayToObject_arity_error(collection, expr): + """Test $arrayToObject errors with wrong number of arguments.""" + result = execute_expression(collection, expr) + assert_expression_result(result, error_code=EXPRESSION_TYPE_MISMATCH_ERROR) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py new file mode 100644 index 000000000..2489e82e3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py @@ -0,0 +1,151 @@ +""" +Expression and field path tests for $arrayToObject expression. + +Tests field path lookups, composite paths, key edge cases, +system variables, and null/missing handling. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Field path lookups +# --------------------------------------------------------------------------- +FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_field_path", + expression={"$arrayToObject": "$a.b"}, + doc={"a": {"b": [{"k": "x", "v": 1}]}}, + expected={"x": 1}, + msg="Should resolve nested field path", + ), + ExpressionTestCase( + id="nonexistent_field_null", + expression={"$arrayToObject": "$a.nonexistent"}, + doc={"a": {"missing": 1}}, + expected=None, + msg="Non-existent field should return null", + ), + ExpressionTestCase( + id="deeply_nested_field", + expression={"$arrayToObject": "$a.b.c"}, + doc={"a": {"b": {"c": [{"k": "x", "v": 1}]}}}, + expected={"x": 1}, + msg="Should resolve deeply nested field path", + ), +] + +# --------------------------------------------------------------------------- +# Composite array path +# --------------------------------------------------------------------------- +COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="composite_array_path", + expression={"$arrayToObject": "$a.b"}, + doc={"a": [{"b": {"k": "x", "v": 1}}, {"b": {"k": "y", "v": 2}}]}, + expected={"x": 1, "y": 2}, + msg="Composite array path should resolve to valid k/v array", + ), +] + +# --------------------------------------------------------------------------- +# Key edge cases +# --------------------------------------------------------------------------- +KEY_EDGE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="empty_string_key", + expression={"$arrayToObject": "$arr"}, + doc={"arr": [{"k": "", "v": 1}]}, + expected={"": 1}, + msg="Should handle empty string key", + ), + ExpressionTestCase( + id="key_with_dots", + expression={"$arrayToObject": "$arr"}, + doc={"arr": [{"k": "a.b.c", "v": 1}]}, + expected={"a.b.c": 1}, + msg="Should handle key with dots", + ), + ExpressionTestCase( + id="key_with_dollar", + expression={"$arrayToObject": "$arr"}, + doc={"arr": [{"k": "$field", "v": 1}]}, + expected={"$field": 1}, + msg="Should handle key with dollar sign", + ), +] + +# --------------------------------------------------------------------------- +# $let and system variables +# --------------------------------------------------------------------------- +SYSTEM_VAR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="let_variable", + expression={"$let": {"vars": {"arr": "$arr"}, "in": {"$arrayToObject": "$$arr"}}}, + doc={"arr": [["a", 1]]}, + expected={"a": 1}, + msg="Should work with $let variable", + ), + ExpressionTestCase( + id="root_variable", + expression={"$arrayToObject": "$$ROOT.pairs"}, + doc={"_id": 1, "pairs": [["a", 1]]}, + expected={"a": 1}, + msg="Should work with $$ROOT", + ), + ExpressionTestCase( + id="current_variable", + expression={"$arrayToObject": "$$CURRENT.pairs"}, + doc={"_id": 2, "pairs": [["a", 1]]}, + expected={"a": 1}, + msg="$$CURRENT should be equivalent to field path", + ), +] + +# --------------------------------------------------------------------------- +# Null/missing via expression +# --------------------------------------------------------------------------- +NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="missing_field", + expression={"$arrayToObject": "$nonexistent"}, + doc={"other": 1}, + expected=None, + msg="Missing field should return null", + ), + ExpressionTestCase( + id="missing_input_type_is_null", + expression={"$type": {"$arrayToObject": "$nonexistent"}}, + doc={"x": 1}, + expected="null", + msg="Missing field should produce null type", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_EXPR_TESTS = ( + FIELD_LOOKUP_TESTS + + COMPOSITE_PATH_TESTS + + KEY_EDGE_TESTS + + SYSTEM_VAR_TESTS + + NULL_MISSING_EXPR_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_EXPR_TESTS)) +def test_arrayToObject_expression(collection, test): + """Test $arrayToObject with field paths and expressions.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_smoke_expression_arrayToObject.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_smoke_expression_arrayToObject.py index d589cb42b..8c49f2e73 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_smoke_expression_arrayToObject.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_smoke_expression_arrayToObject.py @@ -31,4 +31,4 @@ def test_smoke_expression_arrayToObject(collection): ) expected = [{"_id": 1, "obj": {"a": 1, "b": 2}}, {"_id": 2, "obj": {"x": 10, "y": 20}}] - assertSuccess(result, expected, msg="Should support $arrayToObject expression") + assertSuccess(result, expected, "Should support $arrayToObject expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/arrayToObject_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/arrayToObject_common.py new file mode 100644 index 000000000..c51d7eec1 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/arrayToObject_common.py @@ -0,0 +1,15 @@ +""" +Shared test infrastructure for $arrayToObject expression tests. +""" + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class ArrayToObjectTest(BaseTestCase): + """Test case for $arrayToObject operator.""" + + array: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py new file mode 100644 index 000000000..67f87c675 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py @@ -0,0 +1,229 @@ +""" +BSON type element preservation tests for $concatArrays expression. + +Tests that various BSON types are preserved when concatenating arrays, +including special numeric values and boundary values. +""" + +from datetime import datetime, timezone +from uuid import UUID + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.concatArrays.utils.concatArrays_common import ( # noqa: E501 + ConcatArraysTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DOUBLE_NEGATIVE_ZERO, + FLOAT_INFINITY, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT64_MAX, + INT64_MIN, +) + +# --------------------------------------------------------------------------- +# BSON types preserved after concatenation +# --------------------------------------------------------------------------- +BSON_TYPE_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="int64_values", + arrays=[[Int64(1), Int64(2)], [Int64(3)]], + expected=[Int64(1), Int64(2), Int64(3)], + msg="Should preserve Int64 values", + ), + ConcatArraysTest( + id="decimal128_values", + arrays=[[Decimal128("1.5")], [Decimal128("2.5"), Decimal128("3.5")]], + expected=[Decimal128("1.5"), Decimal128("2.5"), Decimal128("3.5")], + msg="Should preserve Decimal128 values", + ), + ConcatArraysTest( + id="datetime_values", + arrays=[ + [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", + ), + ConcatArraysTest( + id="objectid_values", + arrays=[ + [ObjectId("000000000000000000000001")], + [ObjectId("000000000000000000000002")], + ], + expected=[ + ObjectId("000000000000000000000001"), + ObjectId("000000000000000000000002"), + ], + msg="Should preserve ObjectId values", + ), + ConcatArraysTest( + id="binary_values", + arrays=[[Binary(b"\x01", 0)], [Binary(b"\x02", 0)]], + expected=[b"\x01", b"\x02"], + msg="Should preserve Binary values", + ), + ConcatArraysTest( + id="regex_values", + arrays=[[Regex("^a", "i")], [Regex("^b", "i")]], + expected=[Regex("^a", "i"), Regex("^b", "i")], + msg="Should preserve Regex values", + ), + ConcatArraysTest( + id="timestamp_values", + arrays=[[Timestamp(1, 0)], [Timestamp(2, 0)]], + expected=[Timestamp(1, 0), Timestamp(2, 0)], + msg="Should preserve Timestamp values", + ), + ConcatArraysTest( + id="minkey_maxkey", + arrays=[[MinKey()], [MaxKey()]], + expected=[MinKey(), MaxKey()], + msg="Should preserve MinKey/MaxKey values", + ), + ConcatArraysTest( + id="uuid_values", + arrays=[ + [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[ConcatArraysTest] = [ + ConcatArraysTest( + id="mixed_bson_types", + arrays=[[1, "two", Int64(3)], [Decimal128("4"), True, None, MinKey()]], + expected=[1, "two", Int64(3), Decimal128("4"), True, None, MinKey()], + msg="Should concatenate mixed BSON types preserving each", + ), + ConcatArraysTest( + id="mixed_dates_and_ids", + arrays=[ + [datetime(2024, 1, 1, tzinfo=timezone.utc), ObjectId("000000000000000000000001")], + [Timestamp(1, 0), Binary(b"\x01", 0)], + ], + expected=[ + datetime(2024, 1, 1, tzinfo=timezone.utc), + ObjectId("000000000000000000000001"), + Timestamp(1, 0), + b"\x01", + ], + msg="Should concatenate dates, ObjectIds, timestamps, and binary", + ), + ConcatArraysTest( + id="mixed_extremes", + arrays=[[MinKey(), FLOAT_NEGATIVE_INFINITY, None], [FLOAT_INFINITY, MaxKey()]], + expected=[MinKey(), FLOAT_NEGATIVE_INFINITY, None, FLOAT_INFINITY, MaxKey()], + msg="Should concatenate MinKey, MaxKey, infinities, and null", + ), +] + +# --------------------------------------------------------------------------- +# Special numeric values as elements +# --------------------------------------------------------------------------- +SPECIAL_NUMERIC_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="infinity_values", + arrays=[[FLOAT_INFINITY], [FLOAT_NEGATIVE_INFINITY]], + expected=[FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY], + msg="Should preserve infinity values", + ), + ConcatArraysTest( + id="decimal128_infinity", + arrays=[[DECIMAL128_INFINITY], [DECIMAL128_NEGATIVE_INFINITY]], + expected=[DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY], + msg="Should preserve Decimal128 infinity values", + ), + ConcatArraysTest( + id="boundary_values", + arrays=[[INT32_MIN, INT32_MAX], [INT64_MIN, INT64_MAX]], + expected=[INT32_MIN, INT32_MAX, INT64_MIN, INT64_MAX], + msg="Should preserve numeric boundary values", + ), + ConcatArraysTest( + id="negative_zero", + arrays=[[DOUBLE_NEGATIVE_ZERO], [DECIMAL128_NEGATIVE_ZERO]], + expected=[DOUBLE_NEGATIVE_ZERO, DECIMAL128_NEGATIVE_ZERO], + msg="Should preserve negative zero values", + ), +] + +# --------------------------------------------------------------------------- +# Element identity preservation +# --------------------------------------------------------------------------- +ELEMENT_PRESERVATION_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="decimal128_trailing_zeros", + arrays=[[Decimal128("1.0")], [Decimal128("1.00"), Decimal128("1.000")]], + expected=[Decimal128("1.0"), Decimal128("1.00"), Decimal128("1.000")], + msg="Decimal128 trailing zeros preserved", + ), + ConcatArraysTest( + id="decimal128_nan", + arrays=[[DECIMAL128_NAN], [Decimal128("1")]], + expected=[DECIMAL128_NAN, Decimal128("1")], + msg="Decimal128 NaN preserved", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_BSON_TESTS = ( + BSON_TYPE_TESTS + MIXED_BSON_TESTS + SPECIAL_NUMERIC_TESTS + ELEMENT_PRESERVATION_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_BSON_TESTS)) +def test_concatArrays_bson_insert(collection, test): + """Test $concatArrays BSON types with values from inserted documents.""" + doc = {f"arr{i}": a for i, a in enumerate(test.arrays)} + refs = [f"$arr{i}" for i in range(len(test.arrays))] + result = execute_expression_with_insert(collection, {"$concatArrays": refs}, 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_concatArrays_bson_literal(collection, test): + """Test $concatArrays BSON types with literal values.""" + args = [{"$literal": a} if isinstance(a, list) else a for a in test.arrays] + result = execute_expression(collection, {"$concatArrays": args}) + 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/concatArrays/test_expression_concatArrays_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_core_behavior.py new file mode 100644 index 000000000..30d453b9b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_core_behavior.py @@ -0,0 +1,319 @@ +""" +Core behavior tests for $concatArrays expression. + +Tests concatenation of arrays with various element types, empty arrays, +single arrays, multiple arrays, nested arrays, duplicates, null +propagation, and large arrays. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.array.concatArrays.utils.concatArrays_common import ( # noqa: E501 + ConcatArraysTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Success: basic concatenation +# --------------------------------------------------------------------------- +BASIC_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="two_int_arrays", + arrays=[[1, 2], [3, 4]], + expected=[1, 2, 3, 4], + msg="Should concatenate two int arrays", + ), + ConcatArraysTest( + id="two_string_arrays", + arrays=[["a", "b"], ["c", "d"]], + expected=["a", "b", "c", "d"], + msg="Should concatenate two string arrays", + ), + ConcatArraysTest( + id="three_arrays", + arrays=[[1, 2], [3, 4], [5, 6]], + expected=[1, 2, 3, 4, 5, 6], + msg="Should concatenate three arrays", + ), + ConcatArraysTest( + id="mixed_type_elements", + arrays=[[1, "two"], [True, None, {"a": 1}]], + expected=[1, "two", True, None, {"a": 1}], + msg="Should concatenate arrays with mixed types", + ), +] + +# --------------------------------------------------------------------------- +# Success: empty arrays +# --------------------------------------------------------------------------- +EMPTY_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="both_empty", + arrays=[[], []], + expected=[], + msg="Should return empty array for two empty arrays", + ), + ConcatArraysTest( + id="first_empty", + arrays=[[], [1, 2]], + expected=[1, 2], + msg="Should return second array when first is empty", + ), + ConcatArraysTest( + id="second_empty", + arrays=[[1, 2], []], + expected=[1, 2], + msg="Should return first array when second is empty", + ), + ConcatArraysTest( + id="all_empty", + arrays=[[], [], []], + expected=[], + msg="Should return empty array for all empty inputs", + ), + ConcatArraysTest( + id="no_arguments", + arrays=[], + expected=[], + msg="No arguments returns []", + ), + ConcatArraysTest( + id="empty_between_nonempty", + arrays=[[1], [], [2]], + expected=[1, 2], + msg="Empty between non-empty is skipped", + ), + ConcatArraysTest( + id="multiple_empty", + arrays=[[], [], [], []], + expected=[], + msg="Multiple empty arrays returns []", + ), +] + +# --------------------------------------------------------------------------- +# Success: single array +# --------------------------------------------------------------------------- +SINGLE_ARRAY_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="single_array", + arrays=[[1, 2, 3]], + expected=[1, 2, 3], + msg="Should return the single array unchanged", + ), + ConcatArraysTest( + id="single_empty_array", + arrays=[[]], + expected=[], + msg="Should return empty array for single empty input", + ), +] + +# --------------------------------------------------------------------------- +# Success: nested arrays — top-level only +# --------------------------------------------------------------------------- +NESTED_ARRAY_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="nested_subarrays", + arrays=[[[1, 2]], [[3, 4]]], + expected=[[1, 2], [3, 4]], + msg="Should concatenate top-level, not flatten subarrays", + ), + ConcatArraysTest( + id="mixed_nested", + arrays=[[[1], "two"], [[3, 4]]], + expected=[[1], "two", [3, 4]], + msg="Should concatenate mixed nested elements", + ), + ConcatArraysTest( + id="deeply_nested", + arrays=[[[[1]]], [[[2]]]], + expected=[[[1]], [[2]]], + msg="Deep nesting preserved", + ), + ConcatArraysTest( + id="empty_nested", + arrays=[[[]], [[]]], + expected=[[], []], + msg="Empty nested arrays preserved", + ), +] + +# --------------------------------------------------------------------------- +# Success: duplicates +# --------------------------------------------------------------------------- +DUPLICATE_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="duplicate_elements", + arrays=[[1, 2, 3], [2, 3, 4]], + expected=[1, 2, 3, 2, 3, 4], + msg="Should preserve duplicate elements across arrays", + ), + ConcatArraysTest( + id="identical_arrays", + arrays=[[1, 2], [1, 2]], + expected=[1, 2, 1, 2], + msg="Should concatenate identical arrays", + ), +] + +# --------------------------------------------------------------------------- +# Success: null propagation +# --------------------------------------------------------------------------- +NULL_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="null_first_arg", + arrays=[None, [1, 2]], + expected=None, + msg="Should return null when first argument is null", + ), + ConcatArraysTest( + id="null_second_arg", + arrays=[[1, 2], None], + expected=None, + msg="Should return null when second argument is null", + ), + ConcatArraysTest( + id="all_null", + arrays=[None, None], + expected=None, + msg="Should return null when all arguments are null", + ), + ConcatArraysTest( + id="null_among_three", + arrays=[[1], None, [2]], + expected=None, + msg="Should return null when any argument is null", + ), + ConcatArraysTest( + id="null_elements_in_arrays", + arrays=[[1, None], [None, 2]], + expected=[1, None, None, 2], + msg="Should preserve null elements within arrays", + ), +] + +# --------------------------------------------------------------------------- +# Success: arrays of objects +# --------------------------------------------------------------------------- +OBJECT_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="arrays_of_objects", + arrays=[[{"a": 1}], [{"b": 2}]], + expected=[{"a": 1}, {"b": 2}], + msg="Should concatenate arrays of objects", + ), + ConcatArraysTest( + id="objects_with_arrays", + arrays=[[{"items": [1, 2]}], [{"items": [3, 4]}]], + expected=[{"items": [1, 2]}, {"items": [3, 4]}], + msg="Should preserve inner arrays in objects", + ), +] + +# --------------------------------------------------------------------------- +# Success: large arrays +# --------------------------------------------------------------------------- +_LARGE_A = list(range(500)) +_LARGE_B = list(range(500, 1000)) + +LARGE_ARRAY_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="large_arrays", + arrays=[_LARGE_A, _LARGE_B], + expected=list(range(1000)), + msg="Should concatenate large arrays", + ), + ConcatArraysTest( + id="two_5000_arrays", + arrays=[list(range(5000)), list(range(5000, 10000))], + expected=list(range(10000)), + msg="10000 elements from two large arrays", + ), + ConcatArraysTest( + id="one_large_one_small", + arrays=[list(range(10000)), [10000]], + expected=list(range(10001)), + msg="10001 elements from large + small", + ), + ConcatArraysTest( + id="100_single_element_arrays", + arrays=[[i] for i in range(100)], + expected=list(range(100)), + msg="100 single-element arrays concatenated", + ), +] + +# --------------------------------------------------------------------------- +# Success: many arrays +# --------------------------------------------------------------------------- +MANY_ARRAYS_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="five_arrays", + arrays=[[1], [2], [3], [4], [5]], + expected=[1, 2, 3, 4, 5], + msg="Should concatenate five arrays", + ), + ConcatArraysTest( + id="ten_empty_arrays", + arrays=[[] for _ in range(10)], + expected=[], + msg="Should concatenate ten empty arrays", + ), + ConcatArraysTest( + id="fifty_arrays", + arrays=[[i] for i in range(50)], + expected=list(range(50)), + msg="50 arrays concatenated", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + BASIC_TESTS + + EMPTY_TESTS + + SINGLE_ARRAY_TESTS + + NESTED_ARRAY_TESTS + + DUPLICATE_TESTS + + NULL_TESTS + + OBJECT_TESTS + + LARGE_ARRAY_TESTS + + MANY_ARRAYS_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_concatArrays_insert(collection, test): + """Test $concatArrays with values from inserted documents.""" + doc = {f"arr{i}": a for i, a in enumerate(test.arrays)} + refs = [f"$arr{i}" for i in range(len(test.arrays))] + result = execute_expression_with_insert(collection, {"$concatArrays": refs}, 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 + EMPTY_TESTS[0], # both_empty + SINGLE_ARRAY_TESTS[0], # single_array + NESTED_ARRAY_TESTS[0], # nested_subarrays +] + + +@pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) +def test_concatArrays_literal(collection, test): + """Test $concatArrays with literal values.""" + args = [{"$literal": a} if isinstance(a, list) else a for a in test.arrays] + result = execute_expression(collection, {"$concatArrays": args}) + 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/concatArrays/test_expression_concatArrays_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py new file mode 100644 index 000000000..fdca74b26 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py @@ -0,0 +1,318 @@ +""" +Error tests for $concatArrays expression. + +Tests non-array input (all BSON types, special numeric values, boundary values, +string edge cases). $concatArrays propagates null but errors on non-array, +non-null input. +""" + +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.concatArrays.utils.concatArrays_common import ( # noqa: E501 + ConcatArraysTest, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import CONCAT_ARRAYS_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[ConcatArraysTest] = [ + ConcatArraysTest( + id="string_input", + arrays=["hello", [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject string input", + ), + ConcatArraysTest( + id="int_input", + arrays=[42, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject int input", + ), + ConcatArraysTest( + id="negative_int_input", + arrays=[-42, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject negative int input", + ), + ConcatArraysTest( + id="bool_input", + arrays=[True, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject bool input", + ), + ConcatArraysTest( + id="object_input", + arrays=[{"a": 1}, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject object input", + ), + ConcatArraysTest( + id="double_input", + arrays=[3.14, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject double input", + ), + ConcatArraysTest( + id="negative_double_input", + arrays=[-3.14, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject negative double input", + ), + ConcatArraysTest( + id="decimal128_input", + arrays=[Decimal128("1"), [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject decimal128 input", + ), + ConcatArraysTest( + id="int64_input", + arrays=[Int64(1), [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject int64 input", + ), + ConcatArraysTest( + id="objectid_input", + arrays=[ObjectId(), [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject objectid input", + ), + ConcatArraysTest( + id="datetime_input", + arrays=[datetime(2024, 1, 1), [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject datetime input", + ), + ConcatArraysTest( + id="binary_input", + arrays=[Binary(b"x", 0), [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject binary input", + ), + ConcatArraysTest( + id="regex_input", + arrays=[Regex("x"), [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject regex input", + ), + ConcatArraysTest( + id="maxkey_input", + arrays=[MaxKey(), [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject maxkey input", + ), + ConcatArraysTest( + id="minkey_input", + arrays=[MinKey(), [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject minkey input", + ), + ConcatArraysTest( + id="timestamp_input", + arrays=[Timestamp(0, 0), [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject timestamp input", + ), + ConcatArraysTest( + id="non_array_second_arg", + arrays=[[1], 42], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject non-array in second position", + ), + ConcatArraysTest( + id="non_array_middle_arg", + arrays=[[1], "bad", [2]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject non-array in middle position", + ), +] + +# --------------------------------------------------------------------------- +# Error: special float/Decimal128 values +# --------------------------------------------------------------------------- +SPECIAL_NUMERIC_ERROR_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="nan_input", + arrays=[FLOAT_NAN, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject NaN input", + ), + ConcatArraysTest( + id="inf_input", + arrays=[FLOAT_INFINITY, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject Infinity input", + ), + ConcatArraysTest( + id="neg_inf_input", + arrays=[FLOAT_NEGATIVE_INFINITY, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject -Infinity input", + ), + ConcatArraysTest( + id="neg_zero_input", + arrays=[DOUBLE_NEGATIVE_ZERO, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject negative zero input", + ), + ConcatArraysTest( + id="decimal128_nan_input", + arrays=[DECIMAL128_NAN, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 NaN input", + ), + ConcatArraysTest( + id="decimal128_neg_nan_input", + arrays=[Decimal128("-NaN"), [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 -NaN input", + ), + ConcatArraysTest( + id="decimal128_inf_input", + arrays=[DECIMAL128_INFINITY, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 Infinity input", + ), + ConcatArraysTest( + id="decimal128_neg_inf_input", + arrays=[DECIMAL128_NEGATIVE_INFINITY, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 -Infinity input", + ), + ConcatArraysTest( + id="decimal128_neg_zero_input", + arrays=[DECIMAL128_NEGATIVE_ZERO, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject Decimal128 -0 input", + ), +] + +# --------------------------------------------------------------------------- +# Error: numeric boundary values +# --------------------------------------------------------------------------- +BOUNDARY_ERROR_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="int32_max_input", + arrays=[INT32_MAX, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject INT32_MAX input", + ), + ConcatArraysTest( + id="int32_min_input", + arrays=[INT32_MIN, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject INT32_MIN input", + ), + ConcatArraysTest( + id="int64_max_input", + arrays=[INT64_MAX, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject INT64_MAX input", + ), + ConcatArraysTest( + id="int64_min_input", + arrays=[INT64_MIN, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject INT64_MIN input", + ), + ConcatArraysTest( + id="decimal128_max_input", + arrays=[DECIMAL128_MAX, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject DECIMAL128_MAX input", + ), + ConcatArraysTest( + id="decimal128_min_input", + arrays=[DECIMAL128_MIN, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject DECIMAL128_MIN input", + ), +] + +# --------------------------------------------------------------------------- +# Error: string edge cases +# --------------------------------------------------------------------------- +STRING_EDGE_ERROR_TESTS: list[ConcatArraysTest] = [ + ConcatArraysTest( + id="comma_separated_string_input", + arrays=["1, 2, 3", [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject comma-separated string", + ), + ConcatArraysTest( + id="json_like_string_input", + arrays=["[1, 2, 3]", [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject JSON-like string", + ), + ConcatArraysTest( + id="empty_object_input", + arrays=[{}, [1]], + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Should reject empty object as arg", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_TESTS = ( + NOT_ARRAY_ERROR_TESTS + + SPECIAL_NUMERIC_ERROR_TESTS + + BOUNDARY_ERROR_TESTS + + STRING_EDGE_ERROR_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_concatArrays_not_array_insert(collection, test): + """Test $concatArrays error with non-array input from inserted documents.""" + doc = {f"arr{i}": a for i, a in enumerate(test.arrays)} + refs = [f"$arr{i}" for i in range(len(test.arrays))] + result = execute_expression_with_insert(collection, {"$concatArrays": refs}, doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +TEST_SUBSET_FOR_LITERAL = [ + NOT_ARRAY_ERROR_TESTS[0], # string_input + NOT_ARRAY_ERROR_TESTS[-3], # timestamp_input + 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_concatArrays_not_array_literal(collection, test): + """Test $concatArrays error with non-array literal input.""" + args = [{"$literal": a} if isinstance(a, list) else a for a in test.arrays] + result = execute_expression(collection, {"$concatArrays": args}) + 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/concatArrays/test_expression_concatArrays_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py new file mode 100644 index 000000000..204db7f0b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py @@ -0,0 +1,366 @@ +""" +Expression and field path tests for $concatArrays 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 ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import CONCAT_ARRAYS_NOT_ARRAY_ERROR +from documentdb_tests.framework.parametrize import pytest_params + +# --------------------------------------------------------------------------- +# Field path lookups +# --------------------------------------------------------------------------- +FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_field_path", + expression={"$concatArrays": ["$a.b", "$a.c"]}, + doc={"a": {"b": [1, 2], "c": [3, 4]}}, + expected=[1, 2, 3, 4], + msg="Should resolve nested field paths", + ), + ExpressionTestCase( + id="deeply_nested_field", + expression={"$concatArrays": ["$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={"$concatArrays": ["$a.nonexistent", "$b"]}, + doc={"a": {"missing": 1}, "b": [1]}, + expected=None, + msg="Non-existent field should propagate null", + ), + ExpressionTestCase( + id="array_index_path", + expression={"$concatArrays": ["$a.0", [5]]}, + doc={"a": [[1, 2], [3, 4]]}, + expected=[5], + msg="$a.0 resolves to [] in expression context", + ), + ExpressionTestCase( + id="nonexistent_nested_path_empty", + expression={"$concatArrays": ["$f.x", [3]]}, + doc={"f": [{"g": 1}, {"g": 2}]}, + expected=[3], + msg="Non-existent nested path resolves to empty array", + ), + ExpressionTestCase( + id="nested_array_of_object_path", + expression={"$concatArrays": ["$a.b.c", [3]]}, + doc={"a": {"b": [{"c": [1]}, {"c": [2]}]}}, + expected=[[1], [2], 3], + msg="Deep nested path resolved", + ), +] + +# --------------------------------------------------------------------------- +# Composite array paths +# --------------------------------------------------------------------------- +COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="composite_array", + expression={"$concatArrays": ["$x.y", [100]]}, + doc={"x": [{"y": 10}, {"y": 20}]}, + expected=[10, 20, 100], + msg="Composite array path from array-of-objects", + ), + ExpressionTestCase( + id="composite_path_tags", + expression={"$concatArrays": ["$items.tags", ["d"]]}, + doc={"items": [{"tags": ["a", "b"]}, {"tags": ["c"]}]}, + expected=[["a", "b"], ["c"], "d"], + msg="$items.tags resolves to array of arrays", + ), +] + +# --------------------------------------------------------------------------- +# $let and system variables +# --------------------------------------------------------------------------- +LET_AND_VARIABLE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="let_variable", + expression={ + "$let": { + "vars": {"a": "$arr1", "b": "$arr2"}, + "in": {"$concatArrays": ["$$a", "$$b"]}, + } + }, + doc={"arr1": [1, 2], "arr2": [3, 4]}, + expected=[1, 2, 3, 4], + msg="Should work with $let variables", + ), + ExpressionTestCase( + id="root_variable", + expression={"$concatArrays": ["$$ROOT.a", "$$ROOT.b"]}, + doc={"_id": 1, "a": [1], "b": [2]}, + expected=[1, 2], + msg="Should work with $$ROOT", + ), + ExpressionTestCase( + id="current_variable", + expression={"$concatArrays": ["$$CURRENT.a", "$$CURRENT.b"]}, + doc={"_id": 2, "a": [1], "b": [2]}, + expected=[1, 2], + msg="$$CURRENT should be equivalent to field path", + ), + ExpressionTestCase( + id="let_null_variable", + expression={ + "$let": { + "vars": {"x": None}, + "in": {"$concatArrays": ["$$x", [1]]}, + } + }, + doc={"_placeholder": 1}, + expected=None, + msg="$let null variable returns null", + ), +] + +# --------------------------------------------------------------------------- +# Null/missing via expression +# --------------------------------------------------------------------------- +NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="missing_field", + expression={"$concatArrays": ["$nonexistent", [1]]}, + doc={"other": 1}, + expected=None, + msg="Missing field should propagate null", + ), + ExpressionTestCase( + id="missing_input_type_is_null", + expression={"$type": {"$concatArrays": ["$nonexistent", [1]]}}, + doc={"x": 1}, + expected="null", + msg="Missing field should produce null type", + ), + ExpressionTestCase( + id="remove_variable", + expression={"$concatArrays": ["$$REMOVE", [1]]}, + doc={"x": 1}, + expected=None, + msg="$$REMOVE propagates null", + ), + ExpressionTestCase( + id="field_ref_wrapped_non_array", + expression={"$concatArrays": ["$a", [1]]}, + doc={"a": 1}, + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Field resolving to non-array should error", + ), + ExpressionTestCase( + id="missing_first_field", + expression={"$concatArrays": ["$a", "$b"]}, + doc={"b": [1]}, + expected=None, + msg="Missing first field returns null", + ), + ExpressionTestCase( + id="missing_last_field", + expression={"$concatArrays": ["$a", "$b"]}, + doc={"a": [1]}, + expected=None, + msg="Missing last field returns null", + ), + ExpressionTestCase( + id="missing_middle_field", + expression={"$concatArrays": ["$a", "$b", "$c"]}, + doc={"a": [1], "c": [3]}, + expected=None, + msg="Missing middle field returns null", + ), + ExpressionTestCase( + id="all_missing_fields", + expression={"$concatArrays": ["$a", "$b"]}, + doc={"_placeholder": 1}, + expected=None, + msg="All missing fields returns null", + ), + ExpressionTestCase( + id="explicit_null_field", + expression={"$concatArrays": ["$a", "$b"]}, + doc={"a": None, "b": [1]}, + expected=None, + msg="Explicit null field returns null", + ), + ExpressionTestCase( + id="missing_plus_null", + expression={"$concatArrays": ["$not_a_field", "$null_val"]}, + doc={"null_val": None}, + expected=None, + msg="Missing + null returns null", + ), + ExpressionTestCase( + id="null_precedes_non_array", + expression={"$concatArrays": ["$arr", "$null_val", "$int_val"]}, + doc={"arr": [1, 2], "null_val": None, "int_val": 42}, + expected=None, + msg="Null precedes non-array type error", + ), + ExpressionTestCase( + id="null_result_type_is_null", + expression={"$type": {"$concatArrays": ["$a", "$nonexistent"]}}, + doc={"a": [1]}, + expected="null", + msg="$type returns 'null' not 'missing'", + ), +] + +# --------------------------------------------------------------------------- +# Self-composition +# --------------------------------------------------------------------------- +SELF_COMPOSITION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="nested_concatArrays", + expression={"$concatArrays": [{"$concatArrays": ["$a", "$b"]}, "$c"]}, + doc={"a": [1], "b": [2], "c": [3]}, + expected=[1, 2, 3], + msg="Nested $concatArrays should work", + ), + ExpressionTestCase( + id="double_nested_concatArrays", + expression={ + "$concatArrays": [{"$concatArrays": ["$a", "$b"]}, {"$concatArrays": ["$c", "$d"]}] + }, + doc={"a": [1], "b": [2], "c": [3], "d": [4]}, + expected=[1, 2, 3, 4], + msg="Both args are nested $concatArrays", + ), + ExpressionTestCase( + id="triple_depth_concatArrays", + expression={ + "$concatArrays": [{"$concatArrays": [{"$concatArrays": ["$a", "$b"]}, "$c"]}, "$d"] + }, + doc={"a": [1], "b": [2], "c": [3], "d": [4]}, + expected=[1, 2, 3, 4], + msg="Triple nesting depth", + ), +] + +# --------------------------------------------------------------------------- +# Same field referenced multiple times +# --------------------------------------------------------------------------- +SAME_FIELD_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="same_field_twice", + expression={"$concatArrays": ["$a", "$a"]}, + doc={"a": [1, 2, 3]}, + expected=[1, 2, 3, 1, 2, 3], + msg="Same field twice doubles elements", + ), + ExpressionTestCase( + id="same_field_three_times", + expression={"$concatArrays": ["$a", "$a", "$a"]}, + doc={"a": [1]}, + expected=[1, 1, 1], + msg="Same field three times triples elements", + ), + ExpressionTestCase( + id="self_concat_mixed_types", + expression={"$concatArrays": ["$a", "$a"]}, + doc={"a": [42, "string", {"key": "value"}, [1, 2], True]}, + expected=[ + 42, + "string", + {"key": "value"}, + [1, 2], + True, + 42, + "string", + {"key": "value"}, + [1, 2], + True, + ], + msg="Self-concat preserves all types", + ), +] + +# --------------------------------------------------------------------------- +# Array expression and object expression inputs +# --------------------------------------------------------------------------- +EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="array_expression_input", + expression={"$concatArrays": [["$x", "$y"], [3]]}, + doc={"x": 1, "y": 2}, + expected=[1, 2, 3], + msg="Array expression with field refs resolved", + ), + ExpressionTestCase( + id="object_expression_input", + expression={"$concatArrays": [{"a": "$x"}]}, + doc={"x": 1}, + error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, + msg="Object expression is not array", + ), + ExpressionTestCase( + id="literal_then_field", + expression={"$concatArrays": [[1, 2, 3], "$a"]}, + doc={"a": [1, 2]}, + expected=[1, 2, 3, 1, 2], + msg="Literal + field order preserved", + ), + ExpressionTestCase( + id="field_then_literal", + expression={"$concatArrays": ["$a", [1, 2, 3]]}, + doc={"a": [1, 2]}, + expected=[1, 2, 1, 2, 3], + msg="Field + literal order preserved", + ), + ExpressionTestCase( + id="four_fields_with_empty_and_literal", + expression={"$concatArrays": ["$a", "$b", "$c", "$d", [], ["array"]]}, + doc={"a": [1, 2], "b": [3, 4], "c": [5, 6], "d": []}, + expected=[1, 2, 3, 4, 5, 6, "array"], + msg="Multiple fields + literals concatenated", + ), +] + +# --------------------------------------------------------------------------- +# Special object keys +# --------------------------------------------------------------------------- +SPECIAL_KEY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="special_object_keys", + expression={"$concatArrays": ["$a", "$b"]}, + doc={"a": [{"a.b": 1}], "b": [{"$x": 2}]}, + expected=[{"a.b": 1}, {"$x": 2}], + msg="Objects with special keys preserved", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate and test +# --------------------------------------------------------------------------- +ALL_EXPR_TESTS = ( + FIELD_LOOKUP_TESTS + + COMPOSITE_PATH_TESTS + + LET_AND_VARIABLE_TESTS + + NULL_MISSING_EXPR_TESTS + + SELF_COMPOSITION_TESTS + + SAME_FIELD_TESTS + + EXPRESSION_INPUT_TESTS + + SPECIAL_KEY_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_EXPR_TESTS)) +def test_concatArrays_expression(collection, test): + """Test $concatArrays with field paths and expressions.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_smoke_expression_concatArrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_smoke_expression_concatArrays.py index 62ffe6471..eae73d8f0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_smoke_expression_concatArrays.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_smoke_expression_concatArrays.py @@ -28,4 +28,4 @@ def test_smoke_expression_concatArrays(collection): ) expected = [{"_id": 1, "combined": [1, 2, 3, 4]}, {"_id": 2, "combined": [5, 6, 7, 8]}] - assertSuccess(result, expected, msg="Should support $concatArrays expression") + assertSuccess(result, expected, "Should support $concatArrays expression") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/concatArrays_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/concatArrays_common.py new file mode 100644 index 000000000..a2a0d521f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/concatArrays_common.py @@ -0,0 +1,15 @@ +""" +Shared test infrastructure for $concatArrays expression tests. +""" + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class ConcatArraysTest(BaseTestCase): + """Test case for $concatArrays operator.""" + + arrays: Any = None From 1aab00cf8b2ee5165a9c18e787d526f625faf11d Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 30 Jun 2026 16:00:42 -0700 Subject: [PATCH 2/9] apply review guide changes Signed-off-by: Alina (Xi) Li --- ...st_expression_arrayElemAt_core_behavior.py | 85 +++++----- ...st_expression_arrayElemAt_element_types.py | 50 +++--- .../test_expression_arrayElemAt_errors.py | 121 ++++++-------- ...test_expression_arrayElemAt_expressions.py | 10 -- ...est_expression_arrayElemAt_null_missing.py | 34 ++-- ...st_expression_arrayElemAt_numeric_index.py | 50 +++--- ...st_expression_arrayElemAt_out_of_bounds.py | 34 ++-- ...est_expression_arrayToObject_bson_types.py | 158 ++++++++---------- ..._expression_arrayToObject_core_behavior.py | 150 +++++++---------- .../test_expression_arrayToObject_errors.py | 145 +++++++--------- ...st_expression_arrayToObject_expressions.py | 47 ++---- ...test_expression_concatArrays_bson_types.py | 63 +++---- ...t_expression_concatArrays_core_behavior.py | 115 ++++++------- .../test_expression_concatArrays_errors.py | 103 +++++------- ...est_expression_concatArrays_expressions.py | 107 +++++------- 15 files changed, 553 insertions(+), 719 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py index c6369bd2e..cee1bfd53 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py @@ -16,130 +16,138 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- -# Success: basic positive index access -# --------------------------------------------------------------------------- +# Property [Positive Index]: $arrayElemAt returns the element at the given positive index. POSITIVE_INDEX_TESTS: list[ArrayElemAtTest] = [ ArrayElemAtTest( - id="first_element", array=[1, 2, 3], idx=0, expected=1, msg="Should return first element" + id="first_element", + array=[1, 2, 3], + idx=0, + expected=1, + msg="$arrayElemAt should return first element", ), ArrayElemAtTest( - id="second_element", array=[1, 2, 3], idx=1, expected=2, msg="Should return second element" + id="second_element", + array=[1, 2, 3], + idx=1, + expected=2, + msg="$arrayElemAt should return second element", ), ArrayElemAtTest( - id="last_element", array=[1, 2, 3], idx=2, expected=3, msg="Should return last element" + id="last_element", + array=[1, 2, 3], + idx=2, + expected=3, + msg="$arrayElemAt should return last element", ), ArrayElemAtTest( id="single_element_array", array=[42], idx=0, expected=42, - msg="Should return single element", + msg="$arrayElemAt should return single element", ), ArrayElemAtTest( id="string_elements", array=["a", "b", "c"], idx=1, expected="b", - msg="Should return string element", + msg="$arrayElemAt should return string element", ), ArrayElemAtTest( id="mixed_types", array=[1, "two", 3.0, True], idx=2, expected=3.0, - msg="Should return element from mixed-type array", + msg="$arrayElemAt should return element from mixed-type array", ), ArrayElemAtTest( id="nested_array_element", array=[[1, 2], [3, 4]], idx=1, expected=[3, 4], - msg="Should return nested array element", + msg="$arrayElemAt should return nested array element", ), ArrayElemAtTest( id="nested_object_element", array=[{"a": 1}, {"b": 2}], idx=0, expected={"a": 1}, - msg="Should return nested object element", + msg="$arrayElemAt should return nested object element", ), ArrayElemAtTest( id="null_element_in_array", array=[None, 1, 2], idx=0, expected=None, - msg="Should return null element", + msg="$arrayElemAt should return null element", ), ArrayElemAtTest( id="bool_element", array=[True, False], idx=1, expected=False, - msg="Should return bool element", + msg="$arrayElemAt should return bool element", ), ] -# --------------------------------------------------------------------------- -# Success: negative index access -# --------------------------------------------------------------------------- +# Property [Negative Index]: $arrayElemAt counts from the end of the array for a negative index. NEGATIVE_INDEX_TESTS: list[ArrayElemAtTest] = [ ArrayElemAtTest( id="last_via_neg1", array=[1, 2, 3], idx=-1, expected=3, - msg="Should return last element via -1", + msg="$arrayElemAt should return last element via -1", ), ArrayElemAtTest( - id="second_to_last", array=[1, 2, 3], idx=-2, expected=2, msg="Should return second to last" + id="second_to_last", + array=[1, 2, 3], + idx=-2, + expected=2, + msg="$arrayElemAt should return second to last", ), ArrayElemAtTest( id="first_via_neg_len", array=[1, 2, 3], idx=-3, expected=1, - msg="Should return first via negative length", + msg="$arrayElemAt should return first via negative length", ), ArrayElemAtTest( id="single_element_neg1", array=[42], idx=-1, expected=42, - msg="Should return single element via -1", + msg="$arrayElemAt should return single element via -1", ), ] -# --------------------------------------------------------------------------- -# Success: duplicate values in array — index-based access unaffected -# --------------------------------------------------------------------------- +# Property [Duplicate Values]: $arrayElemAt selects by position, ignoring duplicate elements. DUPLICATE_VALUE_TESTS: list[ArrayElemAtTest] = [ ArrayElemAtTest( id="dup_first", array=[1, 1, 1], idx=0, expected=1, - msg="Duplicates don't affect index 0", + msg="$arrayElemAt is unaffected by duplicate elements at index 0", ), ArrayElemAtTest( id="dup_last", array=[1, 1, 1], idx=2, expected=1, - msg="Duplicates don't affect last index", + msg="$arrayElemAt is unaffected by duplicate elements at the last index", ), ArrayElemAtTest( id="dup_neg", array=["a", "a", "b", "a"], idx=-1, expected="a", - msg="Duplicates don't affect negative index", + msg="$arrayElemAt is unaffected by duplicate elements at a negative index", ), ] -# --------------------------------------------------------------------------- -# Success: large array -# --------------------------------------------------------------------------- +# Property [Large Array]: $arrayElemAt resolves positions within large arrays. _LARGE_ARRAY_SIZE = 20_000 _LARGE_ARRAY = list(range(_LARGE_ARRAY_SIZE)) @@ -149,47 +157,44 @@ array=_LARGE_ARRAY, idx=0, expected=0, - msg="Should return first in large array", + msg="$arrayElemAt should return first in large array", ), ArrayElemAtTest( id="large_array_last", array=_LARGE_ARRAY, idx=_LARGE_ARRAY_SIZE - 1, expected=_LARGE_ARRAY_SIZE - 1, - msg="Should return last in large array", + msg="$arrayElemAt should return last in large array", ), ArrayElemAtTest( id="large_array_neg1", array=_LARGE_ARRAY, idx=-1, expected=_LARGE_ARRAY_SIZE - 1, - msg="Should return last via -1 in large array", + msg="$arrayElemAt should return last via -1 in large array", ), ArrayElemAtTest( id="large_array_middle", array=_LARGE_ARRAY, idx=_LARGE_ARRAY_SIZE // 2, expected=_LARGE_ARRAY_SIZE // 2, - msg="Should return middle in large array", + msg="$arrayElemAt should return middle in large array", ), ArrayElemAtTest( id="large_array_neg_middle", array=_LARGE_ARRAY, idx=-(_LARGE_ARRAY_SIZE // 4), expected=_LARGE_ARRAY_SIZE - _LARGE_ARRAY_SIZE // 4, - msg="Should return negative middle in large array", + msg="$arrayElemAt should return negative middle in large array", ), ] -# --------------------------------------------------------------------------- -# Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = POSITIVE_INDEX_TESTS + NEGATIVE_INDEX_TESTS + DUPLICATE_VALUE_TESTS + LARGE_ARRAY_TESTS TEST_SUBSET_FOR_LITERAL = [ - POSITIVE_INDEX_TESTS[0], # first_element - NEGATIVE_INDEX_TESTS[0], # last_via_neg1 - LARGE_ARRAY_TESTS[0], # large_array_first + POSITIVE_INDEX_TESTS[0], + NEGATIVE_INDEX_TESTS[0], + LARGE_ARRAY_TESTS[0], ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_element_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_element_types.py index 3fb675bfc..37f6ff94d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_element_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_element_types.py @@ -31,146 +31,140 @@ INT64_MAX, ) -# --------------------------------------------------------------------------- -# Success: various element BSON types stored in array -# --------------------------------------------------------------------------- +# Property [Element Types]: $arrayElemAt returns the element with its original BSON type. ELEMENT_TYPE_TESTS: list[ArrayElemAtTest] = [ ArrayElemAtTest( id="int64_element", array=[Int64(99)], idx=0, expected=Int64(99), - msg="Should return Int64 element", + msg="$arrayElemAt should return Int64 element", ), ArrayElemAtTest( id="decimal128_element", array=[Decimal128("1.5")], idx=0, expected=Decimal128("1.5"), - msg="Should return Decimal128 element", + msg="$arrayElemAt should return Decimal128 element", ), ArrayElemAtTest( id="datetime_element", array=[datetime(2024, 1, 1, tzinfo=timezone.utc)], idx=0, expected=datetime(2024, 1, 1, tzinfo=timezone.utc), - msg="Should return datetime element", + msg="$arrayElemAt should return datetime element", ), ArrayElemAtTest( id="binary_element", array=[Binary(b"\x01\x02", 0)], idx=0, expected=b"\x01\x02", - msg="Should return binary element", + msg="$arrayElemAt should return binary element", ), ArrayElemAtTest( id="regex_element", array=[Regex("^abc", "i")], idx=0, expected=Regex("^abc", "i"), - msg="Should return regex element", + msg="$arrayElemAt should return regex element", ), ArrayElemAtTest( id="objectid_element", array=[ObjectId("000000000000000000000001")], idx=0, expected=ObjectId("000000000000000000000001"), - msg="Should return ObjectId element", + msg="$arrayElemAt should return ObjectId element", ), ArrayElemAtTest( id="minkey_element", array=[MinKey(), 1], idx=0, expected=MinKey(), - msg="Should return MinKey element", + msg="$arrayElemAt should return MinKey element", ), ArrayElemAtTest( id="maxkey_element", array=[1, MaxKey()], idx=1, expected=MaxKey(), - msg="Should return MaxKey element", + msg="$arrayElemAt should return MaxKey element", ), ArrayElemAtTest( id="timestamp_element", array=[Timestamp(0, 0)], idx=0, expected=Timestamp(0, 0), - msg="Should return Timestamp element", + msg="$arrayElemAt should return Timestamp element", ), - # Special float values ArrayElemAtTest( id="float_nan_element", array=[FLOAT_NAN, 1], idx=0, expected=pytest.approx(math.nan, nan_ok=True), - msg="Should return NaN element", + msg="$arrayElemAt should return NaN element", ), ArrayElemAtTest( id="float_infinity_element", array=[FLOAT_INFINITY, 1], idx=0, expected=FLOAT_INFINITY, - msg="Should return Infinity element", + msg="$arrayElemAt should return Infinity element", ), ArrayElemAtTest( id="float_neg_infinity_element", array=[FLOAT_NEGATIVE_INFINITY, 1], idx=0, expected=FLOAT_NEGATIVE_INFINITY, - msg="Should return -Infinity element", + msg="$arrayElemAt should return -Infinity element", ), - # Special Decimal128 values ArrayElemAtTest( id="decimal128_nan_element", array=[DECIMAL128_NAN, 1], idx=0, expected=DECIMAL128_NAN, - msg="Should return Decimal128 NaN element", + msg="$arrayElemAt should return Decimal128 NaN element", ), ArrayElemAtTest( id="decimal128_infinity_element", array=[DECIMAL128_INFINITY, 1], idx=0, expected=DECIMAL128_INFINITY, - msg="Should return Decimal128 Infinity element", + msg="$arrayElemAt should return Decimal128 Infinity element", ), ArrayElemAtTest( id="decimal128_neg_infinity_element", array=[DECIMAL128_NEGATIVE_INFINITY, 1], idx=0, expected=DECIMAL128_NEGATIVE_INFINITY, - msg="Should return Decimal128 -Infinity element", + msg="$arrayElemAt should return Decimal128 -Infinity element", ), - # Boundary integer values as elements ArrayElemAtTest( id="int32_max_element", array=[INT32_MAX, 0], idx=0, expected=INT32_MAX, - msg="Should return INT32_MAX element", + msg="$arrayElemAt should return INT32_MAX element", ), ArrayElemAtTest( id="int64_max_element", array=[INT64_MAX, 0], idx=0, expected=INT64_MAX, - msg="Should return INT64_MAX element", + msg="$arrayElemAt should return INT64_MAX element", ), - # Mixed special values array ArrayElemAtTest( id="mixed_special_last", array=[INT32_MAX, FLOAT_INFINITY, DECIMAL128_NAN], idx=2, expected=DECIMAL128_NAN, - msg="Should return element from mixed special values array", + msg="$arrayElemAt should return element from mixed special values array", ), ] TEST_SUBSET_FOR_LITERAL = [ - ELEMENT_TYPE_TESTS[0], # int64_element - ELEMENT_TYPE_TESTS[9], # float_nan_element - ELEMENT_TYPE_TESTS[-1], # mixed_special_last + ELEMENT_TYPE_TESTS[0], + ELEMENT_TYPE_TESTS[9], + ELEMENT_TYPE_TESTS[-1], ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py index 2446376c9..0fda837e4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py @@ -5,7 +5,7 @@ and wrong arity errors. """ -from datetime import datetime +from datetime import datetime, timezone import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp @@ -27,348 +27,339 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import INT64_MAX, INT64_MIN -# --------------------------------------------------------------------------- -# Error: non-array first argument -# --------------------------------------------------------------------------- +# Property [Array Type Strictness]: $arrayElemAt rejects a non-array first argument. ARRAY_TYPE_ERROR_TESTS: list[ArrayElemAtTest] = [ ArrayElemAtTest( id="string_as_array", array="hello", idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject string as array", + msg="$arrayElemAt should reject string as array", ), ArrayElemAtTest( id="int_as_array", array=42, idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject int as array", + msg="$arrayElemAt should reject int as array", ), ArrayElemAtTest( id="bool_true_as_array", array=True, idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject bool true as array", + msg="$arrayElemAt should reject bool true as array", ), ArrayElemAtTest( id="bool_false_as_array", array=False, idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject bool false as array", + msg="$arrayElemAt should reject bool false as array", ), ArrayElemAtTest( id="object_as_array", array={"a": 1}, idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject object as array", + msg="$arrayElemAt should reject object as array", ), ArrayElemAtTest( id="double_as_array", array=3.14, idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject double as array", + msg="$arrayElemAt should reject double as array", ), ArrayElemAtTest( id="decimal128_as_array", array=Decimal128("1"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject decimal128 as array", + msg="$arrayElemAt should reject decimal128 as array", ), ArrayElemAtTest( id="int64_as_array", array=Int64(1), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject int64 as array", + msg="$arrayElemAt should reject int64 as array", ), ArrayElemAtTest( id="binary_as_array", array=Binary(b"x", 0), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject binary as array", + msg="$arrayElemAt should reject binary as array", ), ArrayElemAtTest( id="datetime_as_array", - array=datetime(2024, 1, 1), + array=datetime(2024, 1, 1, tzinfo=timezone.utc), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject datetime as array", + msg="$arrayElemAt should reject datetime as array", ), ArrayElemAtTest( id="objectid_as_array", array=ObjectId(), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject objectid as array", + msg="$arrayElemAt should reject objectid as array", ), ArrayElemAtTest( id="regex_as_array", array=Regex("x"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject regex as array", + msg="$arrayElemAt should reject regex as array", ), ArrayElemAtTest( id="maxkey_as_array", array=MaxKey(), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject maxkey as array", + msg="$arrayElemAt should reject maxkey as array", ), ArrayElemAtTest( id="minkey_as_array", array=MinKey(), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject minkey as array", + msg="$arrayElemAt should reject minkey as array", ), ArrayElemAtTest( id="timestamp_as_array", array=Timestamp(0, 0), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject timestamp as array", + msg="$arrayElemAt should reject timestamp as array", ), ArrayElemAtTest( id="nan_as_array", array=float("nan"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject NaN as array", + msg="$arrayElemAt should reject NaN as array", ), ArrayElemAtTest( id="inf_as_array", array=float("inf"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject Infinity as array", + msg="$arrayElemAt should reject Infinity as array", ), ArrayElemAtTest( id="decimal128_nan_as_array", array=Decimal128("NaN"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject Decimal128 NaN as array", + msg="$arrayElemAt should reject Decimal128 NaN as array", ), ArrayElemAtTest( id="decimal128_inf_as_array", array=Decimal128("Infinity"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, - msg="Should reject Decimal128 Infinity as array", + msg="$arrayElemAt should reject Decimal128 Infinity as array", ), ] -# --------------------------------------------------------------------------- -# Error: non-numeric index -# --------------------------------------------------------------------------- +# Property [Index Type Strictness]: $arrayElemAt rejects a non-numeric index. INDEX_TYPE_ERROR_TESTS: list[ArrayElemAtTest] = [ ArrayElemAtTest( id="string_index", array=[1, 2], idx="0", error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject string index", + msg="$arrayElemAt should reject string index", ), ArrayElemAtTest( id="bool_true_index", array=[1, 2], idx=True, error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject bool true index", + msg="$arrayElemAt should reject bool true index", ), ArrayElemAtTest( id="bool_false_index", array=[1, 2], idx=False, error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject bool false index", + msg="$arrayElemAt should reject bool false index", ), ArrayElemAtTest( id="array_index", array=[1, 2], idx=[0], error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject array index", + msg="$arrayElemAt should reject array index", ), ArrayElemAtTest( id="object_index", array=[1, 2], idx={"a": 0}, error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject object index", + msg="$arrayElemAt should reject object index", ), ArrayElemAtTest( id="objectid_index", array=[1, 2], idx=ObjectId(), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject objectid index", + msg="$arrayElemAt should reject objectid index", ), ArrayElemAtTest( id="binary_index", array=[1, 2], idx=Binary(b"\x01", 0), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject binary index", + msg="$arrayElemAt should reject binary index", ), ArrayElemAtTest( id="timestamp_index", array=[1, 2], idx=Timestamp(0, 0), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject timestamp index", + msg="$arrayElemAt should reject timestamp index", ), ArrayElemAtTest( id="datetime_index", array=[1, 2], - idx=datetime(2024, 1, 1), + idx=datetime(2024, 1, 1, tzinfo=timezone.utc), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject datetime index", + msg="$arrayElemAt should reject datetime index", ), ArrayElemAtTest( id="maxkey_index", array=[1, 2], idx=MaxKey(), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject maxkey index", + msg="$arrayElemAt should reject maxkey index", ), ArrayElemAtTest( id="minkey_index", array=[1, 2], idx=MinKey(), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject minkey index", + msg="$arrayElemAt should reject minkey index", ), ArrayElemAtTest( id="regex_index", array=[1, 2], idx=Regex("x"), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, - msg="Should reject regex index", + msg="$arrayElemAt should reject regex index", ), ] -# --------------------------------------------------------------------------- -# Error: non-integral numeric index -# --------------------------------------------------------------------------- +# Property [Integral Index]: $arrayElemAt rejects a non-integral or out-of-range numeric index. NON_INTEGRAL_INDEX_TESTS: list[ArrayElemAtTest] = [ ArrayElemAtTest( id="double_fractional_index", array=[1, 2, 3], idx=1.5, error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject fractional double index", + msg="$arrayElemAt should reject fractional double index", ), ArrayElemAtTest( id="decimal128_fractional_index", array=[1, 2, 3], idx=Decimal128("0.5"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject fractional decimal128 index", + msg="$arrayElemAt should reject fractional decimal128 index", ), ArrayElemAtTest( id="double_nan_index", array=[1, 2, 3], idx=float("nan"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject NaN index", + msg="$arrayElemAt should reject NaN index", ), ArrayElemAtTest( id="double_inf_index", array=[1, 2, 3], idx=float("inf"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject infinity index", + msg="$arrayElemAt should reject infinity index", ), ArrayElemAtTest( id="double_neg_inf_index", array=[1, 2, 3], idx=float("-inf"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject -infinity index", + msg="$arrayElemAt should reject -infinity index", ), ArrayElemAtTest( id="decimal128_nan_index", array=[1, 2, 3], idx=Decimal128("NaN"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject decimal128 NaN index", + msg="$arrayElemAt should reject decimal128 NaN index", ), ArrayElemAtTest( id="decimal128_inf_index", array=[1, 2, 3], idx=Decimal128("Infinity"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject decimal128 infinity index", + msg="$arrayElemAt should reject decimal128 infinity index", ), ArrayElemAtTest( id="decimal128_neg_inf_index", array=[1, 2, 3], idx=Decimal128("-Infinity"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject decimal128 -infinity index", + msg="$arrayElemAt should reject decimal128 -infinity index", ), ArrayElemAtTest( id="int64_max_index", array=[1, 2, 3], idx=INT64_MAX, error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject INT64_MAX index", + msg="$arrayElemAt should reject INT64_MAX index", ), ArrayElemAtTest( id="int64_min_index", array=[1, 2, 3], idx=INT64_MIN, error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject INT64_MIN index", + msg="$arrayElemAt should reject INT64_MIN index", ), ArrayElemAtTest( id="large_double_index", array=[1, 2, 3], idx=1.0e18, error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject large double index", + msg="$arrayElemAt should reject large double index", ), ArrayElemAtTest( id="large_neg_double_index", array=[1, 2, 3], idx=-1.0e18, error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject large negative double index", + msg="$arrayElemAt should reject large negative double index", ), ArrayElemAtTest( id="decimal128_beyond_int32", array=[1, 2, 3], idx=Decimal128("2147483648"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject decimal128 beyond int32", + msg="$arrayElemAt should reject decimal128 beyond int32", ), ArrayElemAtTest( id="decimal128_huge", array=[1, 2, 3], idx=Decimal128("9223372036854775808"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, - msg="Should reject huge decimal128 index", + msg="$arrayElemAt should reject huge decimal128 index", ), ] -# --------------------------------------------------------------------------- -# Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ARRAY_TYPE_ERROR_TESTS + INDEX_TYPE_ERROR_TESTS + NON_INTEGRAL_INDEX_TESTS TEST_SUBSET_FOR_LITERAL = [ - ARRAY_TYPE_ERROR_TESTS[0], # string_as_array - INDEX_TYPE_ERROR_TESTS[0], # string_index - NON_INTEGRAL_INDEX_TESTS[0], # double_fractional_index + ARRAY_TYPE_ERROR_TESTS[0], + INDEX_TYPE_ERROR_TESTS[0], + NON_INTEGRAL_INDEX_TESTS[0], ] @@ -392,9 +383,7 @@ def test_arrayElemAt_insert(collection, test): ) -# --------------------------------------------------------------------------- -# Error: wrong arity -# --------------------------------------------------------------------------- +# Property [Arity]: $arrayElemAt requires exactly two arguments. ARITY_ERROR_TESTS = [ pytest.param({"$arrayElemAt": [[1, 2, 3]]}, id="one_arg"), pytest.param({"$arrayElemAt": [[1, 2, 3], 0, 1]}, id="three_args"), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py index 64e516954..29f8d0100 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py @@ -17,9 +17,7 @@ from documentdb_tests.framework.error_codes import ARRAY_ELEM_AT_INDEX_TYPE_ERROR -# --------------------------------------------------------------------------- # Nested expressions -# --------------------------------------------------------------------------- @pytest.mark.parametrize( "expression,expected", [ @@ -77,9 +75,7 @@ def test_arrayElemAt_nested_expression(collection, expression, expected): assert_expression_result(result, expected=expected) -# --------------------------------------------------------------------------- # Field path lookups -# --------------------------------------------------------------------------- @pytest.mark.parametrize( "document,array_ref,idx,expected", [ @@ -97,9 +93,7 @@ def test_arrayElemAt_field_lookup(collection, document, array_ref, idx, expected assert_expression_result(result, expected=expected) -# --------------------------------------------------------------------------- # Field path: path through array of objects -# --------------------------------------------------------------------------- def test_arrayElemAt_path_through_array_of_objects(collection): """Test $arrayElemAt where field path traverses array of objects.""" result = execute_expression_with_insert( @@ -108,9 +102,7 @@ def test_arrayElemAt_path_through_array_of_objects(collection): assert_expression_result(result, expected=10) -# --------------------------------------------------------------------------- # Field path: composite path for index -# --------------------------------------------------------------------------- def test_arrayElemAt_composite_path_for_index(collection): """Test $arrayElemAt with nested field path as index.""" result = execute_expression_with_insert( @@ -135,9 +127,7 @@ def test_arrayElemAt_composite_array_as_index(collection): assert_expression_result(result, error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR) -# --------------------------------------------------------------------------- # Composite path with Decimal128 indices and OOB -# --------------------------------------------------------------------------- @pytest.mark.parametrize( "idx,expected", [ diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_null_missing.py index e40073a96..13ae48ec4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_null_missing.py @@ -17,56 +17,60 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import MISSING -# --------------------------------------------------------------------------- -# Success: null array/index → null (runs both literal and insert) -# --------------------------------------------------------------------------- +# Property [Null Propagation]: $arrayElemAt returns null when the array or index argument is null. NULL_TESTS: list[ArrayElemAtTest] = [ ArrayElemAtTest( - id="null_array", array=None, idx=0, expected=None, msg="Should return null for null array" + id="null_array", + array=None, + idx=0, + expected=None, + msg="$arrayElemAt should return null for null array", ), ArrayElemAtTest( id="null_array_neg_idx", array=None, idx=-1, expected=None, - msg="Should return null for null array with negative index", + msg="$arrayElemAt should return null for null array with negative index", ), ArrayElemAtTest( id="null_index", array=[1, 2], idx=None, expected=None, - msg="Should return null for null index", + msg="$arrayElemAt should return null for null index", ), ArrayElemAtTest( - id="both_null", array=None, idx=None, expected=None, msg="Should return null when both null" + id="both_null", + array=None, + idx=None, + expected=None, + msg="$arrayElemAt should return null when both null", ), ] -# --------------------------------------------------------------------------- -# Success: missing field → null (literal only, MISSING is a field ref) -# --------------------------------------------------------------------------- +# Property [Missing Propagation]: $arrayElemAt returns null when the array or index is missing. LITERAL_ONLY_TESTS: list[ArrayElemAtTest] = [ ArrayElemAtTest( id="missing_array", array=MISSING, idx=0, expected=None, - msg="Should return null for missing array", + msg="$arrayElemAt should return null for missing array", ), ArrayElemAtTest( id="missing_index", array=[1, 2, 3], idx=MISSING, expected=None, - msg="Should return null for missing index", + msg="$arrayElemAt should return null for missing index", ), ] TEST_SUBSET_FOR_LITERAL = [ - NULL_TESTS[0], # null_array - NULL_TESTS[2], # null_index - NULL_TESTS[3], # both_null + NULL_TESTS[0], + NULL_TESTS[2], + NULL_TESTS[3], ] + LITERAL_ONLY_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py index 1213c7e70..16fba620f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py @@ -19,156 +19,154 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import DECIMAL128_NEGATIVE_ZERO, DOUBLE_NEGATIVE_ZERO -# --------------------------------------------------------------------------- -# Success: numeric index types (int, int64, double that is integral) -# --------------------------------------------------------------------------- +# Property [Numeric Index Types]: $arrayElemAt accepts int32, int64, and integral double indexes. NUMERIC_INDEX_TESTS: list[ArrayElemAtTest] = [ ArrayElemAtTest( id="int64_zero_index", array=[10, 20, 30], idx=Int64(0), expected=10, - msg="Should accept Int64 zero index", + msg="$arrayElemAt should accept Int64 zero index", ), ArrayElemAtTest( id="int64_index", array=[10, 20, 30], idx=Int64(1), expected=20, - msg="Should accept Int64 index", + msg="$arrayElemAt should accept Int64 index", ), ArrayElemAtTest( id="double_integral_index", array=[10, 20, 30], idx=2.0, expected=30, - msg="Should accept integral double index", + msg="$arrayElemAt should accept integral double index", ), ArrayElemAtTest( id="decimal128_integral_index", array=[10, 20, 30], idx=Decimal128("0"), expected=10, - msg="Should accept Decimal128 index", + msg="$arrayElemAt should accept Decimal128 index", ), ArrayElemAtTest( id="int64_negative_index", array=[10, 20, 30], idx=Int64(-1), expected=30, - msg="Should accept negative Int64 index", + msg="$arrayElemAt should accept negative Int64 index", ), ArrayElemAtTest( id="double_negative_integral", array=[10, 20, 30], idx=-2.0, expected=20, - msg="Should accept negative integral double index", + msg="$arrayElemAt should accept negative integral double index", ), ArrayElemAtTest( id="negative_zero_index", array=[10, 20, 30], idx=-0.0, expected=10, - msg="Should treat -0.0 as index 0", + msg="$arrayElemAt should treat -0.0 as index 0", ), ArrayElemAtTest( id="decimal128_negative_zero_index", array=[10, 20, 30], idx=DECIMAL128_NEGATIVE_ZERO, expected=10, - msg="Should treat decimal128 -0 as index 0", + msg="$arrayElemAt should treat decimal128 -0 as index 0", ), ArrayElemAtTest( id="double_negative_zero_const", array=[10, 20, 30], idx=DOUBLE_NEGATIVE_ZERO, expected=10, - msg="Should treat double -0 const as index 0", + msg="$arrayElemAt should treat double -0 const as index 0", ), ArrayElemAtTest( id="decimal128_trailing_zero", array=[10, 20, 30], idx=Decimal128("1.0"), expected=20, - msg="Should accept decimal128 with trailing zero", + msg="$arrayElemAt should accept decimal128 with trailing zero", ), ArrayElemAtTest( id="decimal128_subnormal_zero", array=[10, 20, 30], idx=Decimal128("0E-6176"), expected=10, - msg="Should accept decimal128 subnormal zero", + msg="$arrayElemAt should accept decimal128 subnormal zero", ), ArrayElemAtTest( id="decimal128_20E_neg1", array=[10, 20, 30], idx=Decimal128("20E-1"), expected=30, - msg="Should accept decimal128 20E-1 as index 2", + msg="$arrayElemAt should accept decimal128 20E-1 as index 2", ), ArrayElemAtTest( id="decimal128_0_2E1", array=[10, 20, 30], idx=Decimal128("0.2E1"), expected=30, - msg="Should accept decimal128 0.2E1 as index 2", + msg="$arrayElemAt should accept decimal128 0.2E1 as index 2", ), ArrayElemAtTest( id="decimal128_2E0", array=[10, 20, 30], idx=Decimal128("2E0"), expected=30, - msg="Should accept decimal128 2E0 as index 2", + msg="$arrayElemAt should accept decimal128 2E0 as index 2", ), ArrayElemAtTest( id="decimal128_10E_neg1", array=[10, 20, 30], idx=Decimal128("10E-1"), expected=20, - msg="Should accept decimal128 10E-1 as index 1", + msg="$arrayElemAt should accept decimal128 10E-1 as index 1", ), ArrayElemAtTest( id="decimal128_negative_integral_index", array=[10, 20, 30], idx=Decimal128("-1"), expected=30, - msg="Should accept negative Decimal128 integral index", + msg="$arrayElemAt should accept negative Decimal128 integral index", ), ArrayElemAtTest( id="decimal128_neg_10E_neg1", array=[10, 20, 30], idx=Decimal128("-10E-1"), expected=30, - msg="Should accept decimal128 -10E-1 as index -1", + msg="$arrayElemAt should accept decimal128 -10E-1 as index -1", ), ArrayElemAtTest( id="decimal128_0E_pos3", array=[10, 20, 30], idx=Decimal128("0E+3"), expected=10, - msg="Should accept decimal128 0E+3 as index 0", + msg="$arrayElemAt should accept decimal128 0E+3 as index 0", ), ArrayElemAtTest( id="decimal128_0E_neg3", array=[10, 20, 30], idx=Decimal128("0E-3"), expected=10, - msg="Should accept decimal128 0E-3 as index 0", + msg="$arrayElemAt should accept decimal128 0E-3 as index 0", ), ArrayElemAtTest( id="decimal128_1_00000", array=[10, 20, 30], idx=Decimal128("1.00000"), expected=20, - msg="Should accept decimal128 1.00000 as index 1", + msg="$arrayElemAt should accept decimal128 1.00000 as index 1", ), ] TEST_SUBSET_FOR_LITERAL = [ - NUMERIC_INDEX_TESTS[0], # int64_index - NUMERIC_INDEX_TESTS[5], # negative_zero_index - NUMERIC_INDEX_TESTS[-1], # decimal128_1_00000 + NUMERIC_INDEX_TESTS[0], + NUMERIC_INDEX_TESTS[5], + NUMERIC_INDEX_TESTS[-1], ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py index 4ed9b5c23..ebace6efa 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py @@ -19,100 +19,98 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import INT32_MAX, INT32_MIN -# --------------------------------------------------------------------------- -# Success: out-of-bounds index → no result (returns nothing / missing) -# --------------------------------------------------------------------------- +# Property [Out Of Bounds]: $arrayElemAt returns no value when the index is out of bounds. OUT_OF_BOUNDS_TESTS: list[ArrayElemAtTest] = [ ArrayElemAtTest( id="positive_oob", array=[1, 2, 3], idx=15, expected=[{}], - msg="Should return no result for positive OOB", + msg="$arrayElemAt should return no result for positive OOB", ), ArrayElemAtTest( id="positive_oob_by_one", array=[1, 2, 3], idx=3, expected=[{}], - msg="Should return no result for OOB by one", + msg="$arrayElemAt should return no result for OOB by one", ), ArrayElemAtTest( id="negative_oob", array=[1, 2, 3], idx=-4, expected=[{}], - msg="Should return no result for negative OOB", + msg="$arrayElemAt should return no result for negative OOB", ), ArrayElemAtTest( id="negative_oob_large", array=[1, 2, 3], idx=-100, expected=[{}], - msg="Should return no result for large negative OOB", + msg="$arrayElemAt should return no result for large negative OOB", ), ArrayElemAtTest( id="empty_array_idx_0", array=[], idx=0, expected=[{}], - msg="Should return no result for empty array idx 0", + msg="$arrayElemAt should return no result for empty array idx 0", ), ArrayElemAtTest( id="empty_array_neg1", array=[], idx=-1, expected=[{}], - msg="Should return no result for empty array idx -1", + msg="$arrayElemAt should return no result for empty array idx -1", ), ArrayElemAtTest( id="int32_max_oob", array=[1, 2, 3], idx=INT32_MAX, expected=[{}], - msg="Should return no result for INT32_MAX index", + msg="$arrayElemAt should return no result for INT32_MAX index", ), ArrayElemAtTest( id="int32_min_oob", array=[1, 2, 3], idx=INT32_MIN, expected=[{}], - msg="Should return no result for INT32_MIN index", + msg="$arrayElemAt should return no result for INT32_MIN index", ), ArrayElemAtTest( id="single_element_oob_pos", array=[42], idx=1, expected=[{}], - msg="Should return no result for single element OOB positive", + msg="$arrayElemAt should return no result for single element OOB positive", ), ArrayElemAtTest( id="single_element_oob_neg", array=[42], idx=-2, expected=[{}], - msg="Should return no result for single element OOB negative", + msg="$arrayElemAt should return no result for single element OOB negative", ), ArrayElemAtTest( id="decimal128_oob_pos", array=[1, 2, 3], idx=Decimal128("15"), expected=[{}], - msg="Should return no result for Decimal128 positive OOB", + msg="$arrayElemAt should return no result for Decimal128 positive OOB", ), ArrayElemAtTest( id="decimal128_oob_neg", array=[1, 2, 3], idx=Decimal128("-100"), expected=[{}], - msg="Should return no result for Decimal128 negative OOB", + msg="$arrayElemAt should return no result for Decimal128 negative OOB", ), ] TEST_SUBSET_FOR_LITERAL = [ - OUT_OF_BOUNDS_TESTS[0], # positive_oob - OUT_OF_BOUNDS_TESTS[4], # empty_array_idx_0 - OUT_OF_BOUNDS_TESTS[-1], # decimal128_oob_neg + OUT_OF_BOUNDS_TESTS[0], + OUT_OF_BOUNDS_TESTS[4], + OUT_OF_BOUNDS_TESTS[-1], ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py index 7321a28b9..39be0ceec 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py @@ -7,6 +7,7 @@ across both k/v and pair input forms. """ +import math from datetime import datetime, timezone from uuid import UUID @@ -21,7 +22,6 @@ execute_expression, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertSuccess from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import ( DECIMAL128_INFINITY, @@ -39,279 +39,269 @@ INT64_MIN, ) -# --------------------------------------------------------------------------- -# BSON value types preserved (k/v form) -# --------------------------------------------------------------------------- +# Property [Value Types K/V]: $arrayToObject preserves each value's BSON type in k/v form. BSON_KV_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="kv_int64", array=[{"k": "a", "v": Int64(99)}], expected={"a": Int64(99)}, - msg="Should preserve Int64 value", + msg="$arrayToObject should preserve Int64 value", ), ArrayToObjectTest( id="kv_decimal128", array=[{"k": "a", "v": Decimal128("3.14")}], expected={"a": Decimal128("3.14")}, - msg="Should preserve Decimal128 value", + msg="$arrayToObject should preserve Decimal128 value", ), ArrayToObjectTest( id="kv_datetime", array=[{"k": "a", "v": datetime(2024, 1, 1, tzinfo=timezone.utc)}], expected={"a": datetime(2024, 1, 1, tzinfo=timezone.utc)}, - msg="Should preserve datetime value", + msg="$arrayToObject should preserve datetime value", ), ArrayToObjectTest( id="kv_objectid", array=[{"k": "a", "v": ObjectId("000000000000000000000001")}], expected={"a": ObjectId("000000000000000000000001")}, - msg="Should preserve ObjectId value", + msg="$arrayToObject should preserve ObjectId value", ), ArrayToObjectTest( id="kv_bool_false", array=[{"k": "a", "v": False}], expected={"a": False}, - msg="Should preserve false value", + msg="$arrayToObject should preserve false value", ), ArrayToObjectTest( id="kv_bool_true", array=[{"k": "a", "v": True}], expected={"a": True}, - msg="Should preserve true value", + msg="$arrayToObject should preserve true value", ), ArrayToObjectTest( id="kv_null", array=[{"k": "a", "v": None}], expected={"a": None}, - msg="Should preserve null value", + msg="$arrayToObject should preserve null value", ), ArrayToObjectTest( id="kv_regex", array=[{"k": "a", "v": Regex("^abc", "i")}], expected={"a": Regex("^abc", "i")}, - msg="Should preserve regex value", + msg="$arrayToObject should preserve regex value", ), ArrayToObjectTest( id="kv_minkey", array=[{"k": "a", "v": MinKey()}], expected={"a": MinKey()}, - msg="Should preserve MinKey value", + msg="$arrayToObject should preserve MinKey value", ), ArrayToObjectTest( id="kv_maxkey", array=[{"k": "a", "v": MaxKey()}], expected={"a": MaxKey()}, - msg="Should preserve MaxKey value", + msg="$arrayToObject should preserve MaxKey value", ), ArrayToObjectTest( id="kv_binary", array=[{"k": "a", "v": Binary(b"\x01\x02\x03", 0)}], expected={"a": b"\x01\x02\x03"}, - msg="Should preserve Binary value", + msg="$arrayToObject should preserve Binary value", ), ArrayToObjectTest( id="kv_timestamp", array=[{"k": "a", "v": Timestamp(1234567890, 1)}], expected={"a": Timestamp(1234567890, 1)}, - msg="Should preserve Timestamp value", + msg="$arrayToObject should preserve Timestamp value", ), ArrayToObjectTest( id="kv_uuid", array=[{"k": "a", "v": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))}], expected={"a": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))}, - msg="Should preserve UUID binary value", + msg="$arrayToObject should preserve UUID binary value", ), ] -# --------------------------------------------------------------------------- -# BSON value types preserved (pair form) -# --------------------------------------------------------------------------- +# Property [Value Types Pair]: $arrayToObject preserves each value's BSON type in pair form. BSON_PAIR_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="pair_int64", array=[["a", Int64(99)]], expected={"a": Int64(99)}, - msg="Should preserve Int64 value (pair form)", + msg="$arrayToObject should preserve Int64 value (pair form)", ), ArrayToObjectTest( id="pair_decimal128", array=[["a", Decimal128("3.14")]], expected={"a": Decimal128("3.14")}, - msg="Should preserve Decimal128 value (pair form)", + msg="$arrayToObject should preserve Decimal128 value (pair form)", ), ArrayToObjectTest( id="pair_datetime", array=[["a", datetime(2024, 1, 1, tzinfo=timezone.utc)]], expected={"a": datetime(2024, 1, 1, tzinfo=timezone.utc)}, - msg="Should preserve datetime value (pair form)", + msg="$arrayToObject should preserve datetime value (pair form)", ), ArrayToObjectTest( id="pair_objectid", array=[["a", ObjectId("000000000000000000000001")]], expected={"a": ObjectId("000000000000000000000001")}, - msg="Should preserve ObjectId value (pair form)", + msg="$arrayToObject should preserve ObjectId value (pair form)", ), ArrayToObjectTest( id="pair_binary", array=[["a", Binary(b"\x01\x02\x03", 0)]], expected={"a": b"\x01\x02\x03"}, - msg="Should preserve Binary value (pair form)", + msg="$arrayToObject should preserve Binary value (pair form)", ), ArrayToObjectTest( id="pair_timestamp", array=[["a", Timestamp(1234567890, 1)]], expected={"a": Timestamp(1234567890, 1)}, - msg="Should preserve Timestamp value (pair form)", + msg="$arrayToObject should preserve Timestamp value (pair form)", ), ArrayToObjectTest( id="pair_regex", array=[["a", Regex("^abc", "i")]], expected={"a": Regex("^abc", "i")}, - msg="Should preserve regex value (pair form)", + msg="$arrayToObject should preserve regex value (pair form)", ), ArrayToObjectTest( id="pair_minkey", array=[["a", MinKey()]], expected={"a": MinKey()}, - msg="Should preserve MinKey value (pair form)", + msg="$arrayToObject should preserve MinKey value (pair form)", ), ArrayToObjectTest( id="pair_maxkey", array=[["a", MaxKey()]], expected={"a": MaxKey()}, - msg="Should preserve MaxKey value (pair form)", + msg="$arrayToObject should preserve MaxKey value (pair form)", ), ArrayToObjectTest( id="pair_uuid", array=[["a", Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))]], expected={"a": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))}, - msg="Should preserve UUID binary value (pair form)", + msg="$arrayToObject should preserve UUID binary value (pair form)", ), ] -# --------------------------------------------------------------------------- -# Special numeric values -# --------------------------------------------------------------------------- +# Property [Special Numerics]: $arrayToObject preserves NaN, Infinity, and negative zero. SPECIAL_NUMERIC_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="value_infinity", array=[{"k": "a", "v": FLOAT_INFINITY}], expected={"a": FLOAT_INFINITY}, - msg="Should preserve Infinity value", + msg="$arrayToObject should preserve Infinity value", ), ArrayToObjectTest( id="value_neg_infinity", array=[{"k": "a", "v": FLOAT_NEGATIVE_INFINITY}], expected={"a": FLOAT_NEGATIVE_INFINITY}, - msg="Should preserve -Infinity value", + msg="$arrayToObject should preserve -Infinity value", ), ArrayToObjectTest( id="value_neg_zero", array=[{"k": "a", "v": DOUBLE_NEGATIVE_ZERO}], expected={"a": DOUBLE_NEGATIVE_ZERO}, - msg="Should preserve negative zero value", + msg="$arrayToObject should preserve negative zero value", ), ArrayToObjectTest( id="value_decimal128_nan", array=[{"k": "a", "v": DECIMAL128_NAN}], expected={"a": DECIMAL128_NAN}, - msg="Should preserve Decimal128 NaN value", + msg="$arrayToObject should preserve Decimal128 NaN value", ), ArrayToObjectTest( id="value_decimal128_infinity", array=[{"k": "a", "v": DECIMAL128_INFINITY}], expected={"a": DECIMAL128_INFINITY}, - msg="Should preserve Decimal128 Infinity value", + msg="$arrayToObject should preserve Decimal128 Infinity value", ), ArrayToObjectTest( id="value_decimal128_neg_infinity", array=[{"k": "a", "v": DECIMAL128_NEGATIVE_INFINITY}], expected={"a": DECIMAL128_NEGATIVE_INFINITY}, - msg="Should preserve Decimal128 -Infinity value", + msg="$arrayToObject should preserve Decimal128 -Infinity value", ), ArrayToObjectTest( id="value_decimal128_neg_zero", array=[{"k": "a", "v": DECIMAL128_NEGATIVE_ZERO}], expected={"a": DECIMAL128_NEGATIVE_ZERO}, - msg="Should preserve Decimal128 -0 value", + msg="$arrayToObject should preserve Decimal128 -0 value", ), ArrayToObjectTest( id="value_decimal128_high_precision", array=[{"k": "a", "v": Decimal128("1.234567890123456789012345678901234")}], expected={"a": Decimal128("1.234567890123456789012345678901234")}, - msg="Should preserve full Decimal128 precision", + msg="$arrayToObject should preserve full Decimal128 precision", ), ArrayToObjectTest( id="value_decimal128_zero_exponent", array=[{"k": "a", "v": Decimal128("0E+10")}], expected={"a": Decimal128("0E+10")}, - msg="Should preserve Decimal128 exponent notation", + msg="$arrayToObject should preserve Decimal128 exponent notation", ), ArrayToObjectTest( id="value_decimal128_trailing_zeros", array=[{"k": "a", "v": Decimal128("1.00000")}], expected={"a": Decimal128("1.00000")}, - msg="Should preserve Decimal128 trailing zeros", + msg="$arrayToObject should preserve Decimal128 trailing zeros", ), ArrayToObjectTest( id="value_decimal128_subnormal_zero", array=[{"k": "a", "v": Decimal128("0E-6176")}], expected={"a": Decimal128("0E-6176")}, - msg="Should preserve Decimal128 subnormal zero", + msg="$arrayToObject should preserve Decimal128 subnormal zero", ), ] -# --------------------------------------------------------------------------- -# Boundary values -# --------------------------------------------------------------------------- +# Property [Numeric Boundaries]: $arrayToObject preserves numeric boundary values. BOUNDARY_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="value_int32_max", array=[{"k": "a", "v": INT32_MAX}], expected={"a": INT32_MAX}, - msg="Should preserve INT32_MAX value", + msg="$arrayToObject should preserve INT32_MAX value", ), ArrayToObjectTest( id="value_int32_min", array=[{"k": "a", "v": INT32_MIN}], expected={"a": INT32_MIN}, - msg="Should preserve INT32_MIN value", + msg="$arrayToObject should preserve INT32_MIN value", ), ArrayToObjectTest( id="value_int64_max", array=[{"k": "a", "v": INT64_MAX}], expected={"a": INT64_MAX}, - msg="Should preserve INT64_MAX value", + msg="$arrayToObject should preserve INT64_MAX value", ), ArrayToObjectTest( id="value_int64_min", array=[{"k": "a", "v": INT64_MIN}], expected={"a": INT64_MIN}, - msg="Should preserve INT64_MIN value", + msg="$arrayToObject should preserve INT64_MIN value", ), ArrayToObjectTest( id="value_decimal128_max", array=[{"k": "a", "v": DECIMAL128_MAX}], expected={"a": DECIMAL128_MAX}, - msg="Should preserve DECIMAL128_MAX value", + msg="$arrayToObject should preserve DECIMAL128_MAX value", ), ArrayToObjectTest( id="value_decimal128_min", array=[{"k": "a", "v": DECIMAL128_MIN}], expected={"a": DECIMAL128_MIN}, - msg="Should preserve DECIMAL128_MIN value", + msg="$arrayToObject should preserve DECIMAL128_MIN value", ), ] -# --------------------------------------------------------------------------- -# Nested BSON values -# --------------------------------------------------------------------------- +# Property [Nested Values]: $arrayToObject preserves nested arrays and documents as values. NESTED_BSON_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="nested_bson_in_object_value", array=[{"k": "a", "v": {"x": Int64(1), "y": Decimal128("2.5")}}], expected={"a": {"x": Int64(1), "y": Decimal128("2.5")}}, - msg="Should preserve nested BSON types in object value", + msg="$arrayToObject should preserve nested BSON types in object value", ), ArrayToObjectTest( id="nested_bson_in_array_value", @@ -332,49 +322,45 @@ ObjectId("000000000000000000000001"), ] }, - msg="Should preserve nested BSON types in array value", + msg="$arrayToObject should preserve nested BSON types in array value", ), ArrayToObjectTest( id="deeply_nested_bson", array=[{"k": "a", "v": {"x": [{"y": Decimal128("1.5")}, Timestamp(0, 0)]}}], expected={"a": {"x": [{"y": Decimal128("1.5")}, Timestamp(0, 0)]}}, - msg="Should preserve deeply nested BSON types", + msg="$arrayToObject should preserve deeply nested BSON types", ), ArrayToObjectTest( id="nested_array_not_interpreted_as_kv", array=[{"k": "a", "v": [["level2", {"x": 1}]]}], expected={"a": [["level2", {"x": 1}]]}, - msg="Should preserve nested array as value without interpreting as k/v", + msg="$arrayToObject should preserve nested array as value without interpreting as k/v", ), ] -# --------------------------------------------------------------------------- -# Numeric type equivalence — duplicate keys with different numeric types -# --------------------------------------------------------------------------- +# Property [Duplicate Numeric Keys]: last value wins for duplicate keys of differing numeric types. NUMERIC_EQUIVALENCE_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="duplicate_key_int_then_int64", array=[{"k": "a", "v": 1}, {"k": "a", "v": Int64(2)}], expected={"a": Int64(2)}, - msg="Int64 value should win as last duplicate key", + msg="$arrayToObject should keep the last Int64 value for a duplicate key", ), ArrayToObjectTest( id="duplicate_key_int_then_decimal128", array=[{"k": "a", "v": 1}, {"k": "a", "v": Decimal128("2")}], expected={"a": Decimal128("2")}, - msg="Decimal128 value should win as last duplicate key", + msg="$arrayToObject should keep the last Decimal128 value for a duplicate key", ), ArrayToObjectTest( id="duplicate_key_decimal128_then_double", array=[{"k": "a", "v": Decimal128("1")}, {"k": "a", "v": 2.0}], expected={"a": 2.0}, - msg="Double value should win as last duplicate key", + msg="$arrayToObject should keep the last double value for a duplicate key", ), ] -# --------------------------------------------------------------------------- -# Mixed BSON types in a single array -# --------------------------------------------------------------------------- +# Property [Mixed Types]: $arrayToObject preserves multiple mixed BSON value types in one array. MIXED_BSON_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="kv_mixed_bson_types", @@ -396,13 +382,10 @@ "ts": Timestamp(0, 0), "min": MinKey(), }, - msg="Should preserve multiple mixed BSON types in one conversion", + msg="$arrayToObject should preserve multiple mixed BSON types in one conversion", ), ] -# --------------------------------------------------------------------------- -# Aggregate and test -# --------------------------------------------------------------------------- ALL_BSON_TESTS = ( BSON_KV_TESTS + BSON_PAIR_TESTS @@ -414,14 +397,14 @@ ) TEST_SUBSET_FOR_LITERAL = [ - BSON_KV_TESTS[0], # kv_int64 - BSON_KV_TESTS[10], # kv_binary - BSON_KV_TESTS[12], # kv_uuid - BSON_PAIR_TESTS[0], # pair_int64 - SPECIAL_NUMERIC_TESTS[0], # value_infinity - BOUNDARY_TESTS[0], # value_int32_max - NESTED_BSON_TESTS[0], # nested_bson_in_object_value - MIXED_BSON_TESTS[0], # kv_mixed_bson_types + BSON_KV_TESTS[0], + BSON_KV_TESTS[10], + BSON_KV_TESTS[12], + BSON_PAIR_TESTS[0], + SPECIAL_NUMERIC_TESTS[0], + BOUNDARY_TESTS[0], + NESTED_BSON_TESTS[0], + MIXED_BSON_TESTS[0], ] @@ -445,17 +428,12 @@ def test_arrayToObject_bson_insert(collection, test): ) -# --------------------------------------------------------------------------- -# Float NaN — requires special handling since nan != nan -# --------------------------------------------------------------------------- +# Float NaN needs a dedicated test because NaN does not compare equal to itself. def test_arrayToObject_float_nan_value(collection): """Test $arrayToObject preserves float NaN value.""" - import math - result = execute_expression(collection, {"$arrayToObject": {"$literal": [["a", float("nan")]]}}) - assertSuccess( + assert_expression_result( result, - expected=[{"result": True}], - transform=lambda batch: [{"result": math.isnan(batch[0]["result"]["a"])}], - msg="NaN value should be preserved", + expected={"a": pytest.approx(math.nan, nan_ok=True)}, + msg="$arrayToObject should preserve a NaN value", ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py index bdba332ad..ff0c0d360 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py @@ -16,30 +16,27 @@ execute_expression, execute_expression_with_insert, ) -from documentdb_tests.framework.assertions import assertSuccess from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- -# Success: k/v document form — [{k: , v: }, ...] -# --------------------------------------------------------------------------- +# Property [K/V Form]: $arrayToObject builds an object from {k, v} document entries. KV_FORM_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="kv_single_pair", array=[{"k": "a", "v": 1}], expected={"a": 1}, - msg="Should convert single k/v pair", + msg="$arrayToObject should convert single k/v pair", ), ArrayToObjectTest( id="kv_multiple_pairs", array=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "c", "v": 3}], expected={"a": 1, "b": 2, "c": 3}, - msg="Should convert multiple k/v pairs", + msg="$arrayToObject should convert multiple k/v pairs", ), ArrayToObjectTest( id="kv_string_values", array=[{"k": "name", "v": "Alice"}, {"k": "city", "v": "Mycity"}], expected={"name": "Alice", "city": "Mycity"}, - msg="Should convert k/v pairs with string values", + msg="$arrayToObject should convert k/v pairs with string values", ), ArrayToObjectTest( id="kv_mixed_value_types", @@ -50,241 +47,228 @@ {"k": "null", "v": None}, ], expected={"int": 1, "str": "hello", "bool": True, "null": None}, - msg="Should convert k/v pairs with mixed value types", + msg="$arrayToObject should convert k/v pairs with mixed value types", ), ArrayToObjectTest( id="kv_nested_object_value", array=[{"k": "obj", "v": {"x": 1, "y": 2}}], expected={"obj": {"x": 1, "y": 2}}, - msg="Should convert k/v pair with nested object value", + msg="$arrayToObject should convert k/v pair with nested object value", ), ArrayToObjectTest( id="kv_array_value", array=[{"k": "arr", "v": [1, 2, 3]}], expected={"arr": [1, 2, 3]}, - msg="Should convert k/v pair with array value", + msg="$arrayToObject should convert k/v pair with array value", ), ] -# --------------------------------------------------------------------------- -# Success: two-element array form — [[, ], ...] -# --------------------------------------------------------------------------- +# Property [Pair Form]: $arrayToObject builds an object from two-element [key, value] arrays. TWO_ELEM_FORM_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="pair_single", array=[["a", 1]], expected={"a": 1}, - msg="Should convert single two-element pair", + msg="$arrayToObject should convert single two-element pair", ), ArrayToObjectTest( id="pair_multiple", array=[["a", 1], ["b", 2], ["c", 3]], expected={"a": 1, "b": 2, "c": 3}, - msg="Should convert multiple two-element pairs", + msg="$arrayToObject should convert multiple two-element pairs", ), ArrayToObjectTest( id="pair_string_values", array=[["name", "Alice"], ["city", "Mycity"]], expected={"name": "Alice", "city": "Mycity"}, - msg="Should convert pairs with string values", + msg="$arrayToObject should convert pairs with string values", ), ArrayToObjectTest( id="pair_mixed_value_types", array=[["int", 1], ["str", "hello"], ["bool", True], ["null", None]], expected={"int": 1, "str": "hello", "bool": True, "null": None}, - msg="Should convert pairs with mixed value types", + msg="$arrayToObject should convert pairs with mixed value types", ), ArrayToObjectTest( id="pair_nested_object_value", array=[["obj", {"x": 1, "y": 2}]], expected={"obj": {"x": 1, "y": 2}}, - msg="Should convert pair with nested object value", + msg="$arrayToObject should convert pair with nested object value", ), ArrayToObjectTest( id="pair_array_value", array=[["arr", [1, 2, 3]]], expected={"arr": [1, 2, 3]}, - msg="Should convert pair with array value", + msg="$arrayToObject should convert pair with array value", ), ] -# --------------------------------------------------------------------------- -# Success: empty or null -# --------------------------------------------------------------------------- +# Property [Empty And Null]: $arrayToObject returns {} for an empty array and null for null input. EMPTY_AND_NULL_ARRAY_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="empty_array", array=[], expected={}, - msg="Should return empty object for empty array", + msg="$arrayToObject should return empty object for empty array", ), ArrayToObjectTest( id="null_array", array=None, expected=None, - msg="Should return null for null array", + msg="$arrayToObject should return null for null array", ), ] -# --------------------------------------------------------------------------- -# Success: duplicate keys — last value wins -# --------------------------------------------------------------------------- +# Property [Duplicate Keys]: when keys repeat, $arrayToObject keeps the last value. DUPLICATE_KEY_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="kv_duplicate_keys", array=[{"k": "a", "v": 1}, {"k": "a", "v": 2}], expected={"a": 2}, - msg="Last value should win for duplicate keys (k/v form)", + msg="$arrayToObject should keep the last value for duplicate keys (k/v form)", ), ArrayToObjectTest( id="pair_duplicate_keys", array=[["a", 1], ["a", 2]], expected={"a": 2}, - msg="Last value should win for duplicate keys (pair form)", + msg="$arrayToObject should keep the last value for duplicate keys (pair form)", ), ArrayToObjectTest( id="kv_triple_duplicate", array=[{"k": "x", "v": 1}, {"k": "x", "v": 2}, {"k": "x", "v": 3}], expected={"x": 3}, - msg="Last of three duplicate keys should win", + msg="$arrayToObject should keep the last of three duplicate keys", ), ArrayToObjectTest( id="pair_dup_different_types", array=[["a", 1], ["a", "hello"]], expected={"a": "hello"}, - msg="Last value should win even with different value types", + msg="$arrayToObject should keep the last value even with different value types", ), ArrayToObjectTest( id="pair_dup_interspersed", array=[["a", 1], ["b", 2], ["a", 3]], expected={"a": 3, "b": 2}, - msg="Last value should win with interspersed duplicate keys", + msg="$arrayToObject should keep the last value with interspersed duplicate keys", ), ArrayToObjectTest( id="kv_dup_interspersed", array=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "a", "v": 3}], expected={"a": 3, "b": 2}, - msg="Last value should win with interspersed duplicates (k/v form)", + msg="$arrayToObject should keep the last value with interspersed duplicates (k/v form)", ), ArrayToObjectTest( id="kv_reversed_field_order", array=[{"v": "val", "k": "key"}], expected={"key": "val"}, - msg="Should work regardless of k/v field order in document", + msg="$arrayToObject should work regardless of k/v field order in document", ), ] -# --------------------------------------------------------------------------- -# Success: key edge cases (unicode, emoji, spaces) -# --------------------------------------------------------------------------- +# Property [Key Characters]: $arrayToObject accepts unicode, emoji, and spaced keys. KEY_EDGE_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="unicode_key", array=[{"k": "日本語", "v": 1}], expected={"日本語": 1}, - msg="Unicode key should be valid", + msg="$arrayToObject should accept a unicode key", ), ArrayToObjectTest( id="emoji_key", array=[{"k": "🔑", "v": "value"}], expected={"🔑": "value"}, - msg="Emoji key should be valid", + msg="$arrayToObject should accept an emoji key", ), ArrayToObjectTest( id="key_with_spaces", array=[["key with spaces", 1]], expected={"key with spaces": 1}, - msg="Key with spaces should be valid", + msg="$arrayToObject should accept a key with spaces", ), ArrayToObjectTest( id="numeric_string_keys", array=[["0", "a"], ["1", "b"]], expected={"0": "a", "1": "b"}, - msg="Numeric string keys should be treated as strings", + msg="$arrayToObject should treat numeric string keys as strings", ), ArrayToObjectTest( id="underscore_id_key", array=[["_id", 1]], expected={"_id": 1}, - msg="_id key should be valid", + msg="$arrayToObject should accept _id as a key", ), ArrayToObjectTest( id="operator_like_key", array=[["$set", 1]], expected={"$set": 1}, - msg="Operator-like key should be valid", + msg="$arrayToObject should accept an operator-like key", ), ArrayToObjectTest( id="very_long_key", array=[["k" * 1024, 1]], expected={"k" * 1024: 1}, - msg="Very long key should not be truncated", + msg="$arrayToObject should not truncate a very long key", ), ] -# --------------------------------------------------------------------------- -# Success: field ordering, case sensitivity, value/key edge cases -# --------------------------------------------------------------------------- +# Property [Field Ordering]: $arrayToObject preserves input order and treats keys case-sensitively. EDGE_CASE_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="output_field_order", array=[["z", 1], ["a", 2], ["m", 3]], expected={"z": 1, "a": 2, "m": 3}, - msg="Output field order should match input order", + msg="$arrayToObject should preserve input field order in the output", ), ArrayToObjectTest( id="case_sensitive_keys_kv", array=[{"k": "price", "v": 24}, {"k": "PRICE", "v": 100}], expected={"price": 24, "PRICE": 100}, - msg="Case-differing keys should be distinct", + msg="$arrayToObject should treat case-differing keys as distinct", ), ArrayToObjectTest( id="case_sensitive_keys_pair", array=[["price", 24], ["PRICE", 100]], expected={"price": 24, "PRICE": 100}, - msg="Case-differing keys should be distinct (pair form)", + msg="$arrayToObject should treat case-differing keys as distinct (pair form)", ), ArrayToObjectTest( id="deeply_nested_object_value", array=[["key", {"a": {"b": {"c": {"d": 1}}}}]], expected={"key": {"a": {"b": {"c": {"d": 1}}}}}, - msg="Should handle deeply nested object", + msg="$arrayToObject should handle deeply nested object", ), ArrayToObjectTest( id="deeply_nested_array_value", array=[["key", [[[[1]]]]]], expected={"key": [[[[1]]]]}, - msg="Should handle deeply nested array", + msg="$arrayToObject should handle deeply nested array", ), ArrayToObjectTest( id="empty_object_value", array=[["key", {}]], expected={"key": {}}, - msg="Should handle empty object value", + msg="$arrayToObject should handle empty object value", ), ArrayToObjectTest( id="empty_array_value", array=[["key", []]], expected={"key": []}, - msg="Should handle empty array value", + msg="$arrayToObject should handle empty array value", ), ArrayToObjectTest( id="empty_string_value", array=[["key", ""]], expected={"key": ""}, - msg="Should handle empty string value", + msg="$arrayToObject should handle empty string value", ), ArrayToObjectTest( id="large_string_value", array=[["key", "x" * 10240]], expected={"key": "x" * 10240}, - msg="Should handle large string value", + msg="$arrayToObject should handle large string value", ), ] -# --------------------------------------------------------------------------- -# Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( KV_FORM_TESTS + TWO_ELEM_FORM_TESTS @@ -295,12 +279,12 @@ ) TEST_SUBSET_FOR_LITERAL = [ - KV_FORM_TESTS[0], # kv_single_pair - KV_FORM_TESTS[3], # kv_mixed_value_types - TWO_ELEM_FORM_TESTS[0], # pair_single - EMPTY_AND_NULL_ARRAY_TESTS[0], # empty_array - DUPLICATE_KEY_TESTS[0], # kv_duplicate_keys - EDGE_CASE_TESTS[0], # output_field_order + KV_FORM_TESTS[0], + KV_FORM_TESTS[3], + TWO_ELEM_FORM_TESTS[0], + EMPTY_AND_NULL_ARRAY_TESTS[0], + DUPLICATE_KEY_TESTS[0], + EDGE_CASE_TESTS[0], ] @@ -324,41 +308,25 @@ def test_arrayToObject_insert(collection, test): ) -# --------------------------------------------------------------------------- -# Large inputs — standalone because assertSuccess+transform is used to -# spot-check a few fields instead of comparing the full 10K-field object. -# --------------------------------------------------------------------------- def test_arrayToObject_large_array_two_element(collection): """Test $arrayToObject with 10,000 two-element pairs.""" - large_arr = [[f"key_{i}", i] for i in range(10000)] + large_arr = [[f"key_{i}", i] for i in range(10_000)] + expected = {f"key_{i}": i for i in range(10_000)} result = execute_expression(collection, {"$arrayToObject": {"$literal": large_arr}}) - assertSuccess( + assert_expression_result( result, - expected=[{"has_10k": True, "first": 0, "last": 9999}], - transform=lambda batch: [ - { - "has_10k": len(batch[0]["result"]) == 10000, - "first": batch[0]["result"]["key_0"], - "last": batch[0]["result"]["key_9999"], - } - ], - msg="Should produce 10,000 fields with correct values", + expected=expected, + msg="$arrayToObject should build a 10,000-field object from two-element pairs", ) def test_arrayToObject_large_array_kv(collection): """Test $arrayToObject with 10,000 k/v documents.""" - large_arr = [{"k": f"key_{i}", "v": i} for i in range(10000)] + large_arr = [{"k": f"key_{i}", "v": i} for i in range(10_000)] + expected = {f"key_{i}": i for i in range(10_000)} result = execute_expression(collection, {"$arrayToObject": {"$literal": large_arr}}) - assertSuccess( + assert_expression_result( result, - expected=[{"has_10k": True, "first": 0, "last": 9999}], - transform=lambda batch: [ - { - "has_10k": len(batch[0]["result"]) == 10000, - "first": batch[0]["result"]["key_0"], - "last": batch[0]["result"]["key_9999"], - } - ], - msg="Should produce 10,000 fields with correct values in k/v format", + expected=expected, + msg="$arrayToObject should build a 10,000-field object from k/v documents", ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py index 39e344c98..51b982216 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py @@ -34,351 +34,338 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- -# Error: input not an array -# --------------------------------------------------------------------------- +# Property [Array Type Strictness]: $arrayToObject rejects a non-array input. NOT_ARRAY_ERROR_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="string_input", array="hello", error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject string input", + msg="$arrayToObject should reject string input", ), ArrayToObjectTest( id="int_input", array=42, error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject int input", + msg="$arrayToObject should reject int input", ), ArrayToObjectTest( id="bool_input", array=True, error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject bool input", + msg="$arrayToObject should reject bool input", ), ArrayToObjectTest( id="object_input", array={"a": 1}, error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject object input", + msg="$arrayToObject should reject object input", ), ArrayToObjectTest( id="double_input", array=3.14, error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject double input", + msg="$arrayToObject should reject double input", ), ArrayToObjectTest( id="decimal128_input", array=Decimal128("1"), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject decimal128 input", + msg="$arrayToObject should reject decimal128 input", ), ArrayToObjectTest( id="int64_input", array=Int64(1), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject int64 input", + msg="$arrayToObject should reject int64 input", ), ArrayToObjectTest( id="objectid_input", array=ObjectId(), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject objectid input", + msg="$arrayToObject should reject objectid input", ), ArrayToObjectTest( id="datetime_input", array=datetime(2024, 1, 1), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject datetime input", + msg="$arrayToObject should reject datetime input", ), ArrayToObjectTest( id="binary_input", array=Binary(b"x", 0), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject binary input", + msg="$arrayToObject should reject binary input", ), ArrayToObjectTest( id="regex_input", array=Regex("x"), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject regex input", + msg="$arrayToObject should reject regex input", ), ArrayToObjectTest( id="maxkey_input", array=MaxKey(), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject maxkey input", + msg="$arrayToObject should reject maxkey input", ), ArrayToObjectTest( id="minkey_input", array=MinKey(), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject minkey input", + msg="$arrayToObject should reject minkey input", ), ArrayToObjectTest( id="timestamp_input", array=Timestamp(0, 0), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, - msg="Should reject timestamp input", + msg="$arrayToObject should reject timestamp input", ), ] -# --------------------------------------------------------------------------- -# Error: invalid element format (not k/v doc or 2-element array) -# --------------------------------------------------------------------------- +# Property [Element Format]: $arrayToObject rejects an element that is not a k/v doc or pair. INVALID_ELEMENT_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="element_is_string", array=["not_a_pair"], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, - msg="Should reject string element", + msg="$arrayToObject should reject string element", ), ArrayToObjectTest( id="element_is_int", array=[42], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, - msg="Should reject int element", + msg="$arrayToObject should reject int element", ), ArrayToObjectTest( id="element_is_null", array=[None], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, - msg="Should reject null element", + msg="$arrayToObject should reject null element", ), ArrayToObjectTest( id="element_is_bool", array=[True], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, - msg="Should reject bool element", + msg="$arrayToObject should reject bool element", ), ArrayToObjectTest( id="element_is_double", array=[3.14], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, - msg="Should reject double element", + msg="$arrayToObject should reject double element", ), ArrayToObjectTest( id="element_is_objectid", array=[ObjectId()], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, - msg="Should reject ObjectId element", + msg="$arrayToObject should reject ObjectId element", ), ArrayToObjectTest( id="kv_missing_v", array=[{"k": "a"}], error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, - msg="Should reject k/v doc missing v field", + msg="$arrayToObject should reject k/v doc missing v field", ), ArrayToObjectTest( id="kv_missing_k", array=[{"v": 1}], error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, - msg="Should reject k/v doc missing k field", + msg="$arrayToObject should reject k/v doc missing k field", ), ArrayToObjectTest( id="kv_extra_field", array=[{"k": "a", "v": 1, "extra": 2}], error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, - msg="Should reject k/v doc with extra field", + msg="$arrayToObject should reject k/v doc with extra field", ), ArrayToObjectTest( id="kv_empty_doc", array=[{}], error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, - msg="Should reject empty document", + msg="$arrayToObject should reject empty document", ), ArrayToObjectTest( id="kv_wrong_field_names", array=[{"y": "x", "x": "y"}], error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, - msg="Should reject wrong field names", + msg="$arrayToObject should reject wrong field names", ), ArrayToObjectTest( id="kv_uppercase_K", array=[{"K": "k1", "v": 2}], error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, - msg="Should reject uppercase K (case-sensitive)", + msg="$arrayToObject should reject uppercase K (case-sensitive)", ), ArrayToObjectTest( id="kv_uppercase_V", array=[{"k": "k1", "V": 2}], error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, - msg="Should reject uppercase V (case-sensitive)", + msg="$arrayToObject should reject uppercase V (case-sensitive)", ), ArrayToObjectTest( id="kv_key_value_names", array=[{"key": "k1", "value": "v1"}], error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, - msg="Should reject 'key'/'value' instead of 'k'/'v'", + msg="$arrayToObject should reject 'key'/'value' instead of 'k'/'v'", ), ArrayToObjectTest( id="mix_valid_pair_and_invalid", array=[["a", 1], 123], error_code=ARRAY_TO_OBJECT_MIXED_PAIR_THEN_KV_ERROR, - msg="Should reject mix of valid pair and invalid element", + msg="$arrayToObject should reject mix of valid pair and invalid element", ), ArrayToObjectTest( id="pair_one_element", array=[["a"]], error_code=ARRAY_TO_OBJECT_INVALID_PAIR_ERROR, - msg="Should reject one-element array pair", + msg="$arrayToObject should reject one-element array pair", ), ArrayToObjectTest( id="pair_three_elements", array=[["a", 1, 2]], error_code=ARRAY_TO_OBJECT_INVALID_PAIR_ERROR, - msg="Should reject three-element array pair", + msg="$arrayToObject should reject three-element array pair", ), ArrayToObjectTest( id="pair_empty_array", array=[[]], error_code=ARRAY_TO_OBJECT_INVALID_PAIR_ERROR, - msg="Should reject empty array pair", + msg="$arrayToObject should reject empty array pair", ), ] -# --------------------------------------------------------------------------- -# Error: key not a string -# --------------------------------------------------------------------------- +# Property [Key Type Strictness]: $arrayToObject rejects a non-string key. KEY_NOT_STRING_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="kv_int_key", array=[{"k": 1, "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, - msg="Should reject int key in k/v form", + msg="$arrayToObject should reject int key in k/v form", ), ArrayToObjectTest( id="kv_bool_key", array=[{"k": True, "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, - msg="Should reject bool key in k/v form", + msg="$arrayToObject should reject bool key in k/v form", ), ArrayToObjectTest( id="kv_null_key", array=[{"k": None, "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, - msg="Should reject null key in k/v form", + msg="$arrayToObject should reject null key in k/v form", ), ArrayToObjectTest( id="kv_array_key", array=[{"k": [1], "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, - msg="Should reject array key in k/v form", + msg="$arrayToObject should reject array key in k/v form", ), ArrayToObjectTest( id="kv_object_key", array=[{"k": {"x": 1}, "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, - msg="Should reject object key in k/v form", + msg="$arrayToObject should reject object key in k/v form", ), ArrayToObjectTest( id="kv_double_key", array=[{"k": 1.5, "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, - msg="Should reject double key in k/v form", + msg="$arrayToObject should reject double key in k/v form", ), ArrayToObjectTest( id="kv_int64_key", array=[{"k": Int64(1), "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, - msg="Should reject Int64 key in k/v form", + msg="$arrayToObject should reject Int64 key in k/v form", ), ArrayToObjectTest( id="kv_decimal128_key", array=[{"k": Decimal128("1"), "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, - msg="Should reject Decimal128 key in k/v form", + msg="$arrayToObject should reject Decimal128 key in k/v form", ), ArrayToObjectTest( id="pair_int_key", array=[[1, "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, - msg="Should reject int key in pair form", + msg="$arrayToObject should reject int key in pair form", ), ArrayToObjectTest( id="pair_bool_key", array=[[True, "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, - msg="Should reject bool key in pair form", + msg="$arrayToObject should reject bool key in pair form", ), ArrayToObjectTest( id="pair_null_key", array=[[None, "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, - msg="Should reject null key in pair form", + msg="$arrayToObject should reject null key in pair form", ), ArrayToObjectTest( id="pair_array_key", array=[[[1], "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, - msg="Should reject array key in pair form", + msg="$arrayToObject should reject array key in pair form", ), ArrayToObjectTest( id="pair_object_key", array=[[{"x": 1}, "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, - msg="Should reject object key in pair form", + msg="$arrayToObject should reject object key in pair form", ), ArrayToObjectTest( id="pair_double_key", array=[[1.5, "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, - msg="Should reject double key in pair form", + msg="$arrayToObject should reject double key in pair form", ), ArrayToObjectTest( id="pair_int64_key", array=[[Int64(1), "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, - msg="Should reject Int64 key in pair form", + msg="$arrayToObject should reject Int64 key in pair form", ), ArrayToObjectTest( id="pair_decimal128_key", array=[[Decimal128("1"), "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, - msg="Should reject Decimal128 key in pair form", + msg="$arrayToObject should reject Decimal128 key in pair form", ), ] -# --------------------------------------------------------------------------- -# Error: mixed formats (k/v then pair, pair then k/v) -# --------------------------------------------------------------------------- +# Property [Mixed Formats]: $arrayToObject rejects arrays mixing k/v doc and pair forms. MIXED_FORMAT_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="mixed_kv_then_pair", array=[{"k": "price", "v": 24}, ["item", "apple"]], error_code=ARRAY_TO_OBJECT_MIXED_KV_THEN_PAIR_ERROR, - msg="k/v doc followed by pair should fail", + msg="$arrayToObject should reject a k/v doc followed by a pair", ), ArrayToObjectTest( id="mixed_pair_then_kv", array=[["item", "apple"], {"k": "price", "v": 24}], error_code=ARRAY_TO_OBJECT_MIXED_PAIR_THEN_KV_ERROR, - msg="Pair followed by k/v doc should fail", + msg="$arrayToObject should reject a pair followed by a k/v doc", ), ] -# --------------------------------------------------------------------------- -# Error: null byte in key -# --------------------------------------------------------------------------- +# Property [Null Byte Key]: $arrayToObject rejects a key containing a null byte. NULL_BYTE_KEY_TESTS: list[ArrayToObjectTest] = [ ArrayToObjectTest( id="null_byte_in_key_pair", array=[["a\x00b", "value"]], error_code=ARRAY_TO_OBJECT_NULL_BYTE_PAIR_KEY_ERROR, - msg="Null byte in key should fail (pair form)", + msg="$arrayToObject should reject a null byte in a key (pair form)", ), ArrayToObjectTest( id="null_byte_in_key_kv", array=[{"k": "a\x00b", "v": "value"}], error_code=ARRAY_TO_OBJECT_NULL_BYTE_KV_KEY_ERROR, - msg="Null byte in key should fail (k/v form)", + msg="$arrayToObject should reject a null byte in a key (k/v form)", ), ] -# --------------------------------------------------------------------------- -# Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( NOT_ARRAY_ERROR_TESTS + INVALID_ELEMENT_TESTS @@ -400,13 +387,13 @@ def test_arrayToObject_insert(collection, test): TEST_SUBSET_FOR_LITERAL = [ - NOT_ARRAY_ERROR_TESTS[0], # string_input - NOT_ARRAY_ERROR_TESTS[3], # object_input - INVALID_ELEMENT_TESTS[0], # element_is_string - INVALID_ELEMENT_TESTS[6], # kv_missing_v - KEY_NOT_STRING_TESTS[0], # kv_int_key - KEY_NOT_STRING_TESTS[8], # pair_int_key - MIXED_FORMAT_TESTS[0], # mixed_kv_then_pair + NOT_ARRAY_ERROR_TESTS[0], + NOT_ARRAY_ERROR_TESTS[3], + INVALID_ELEMENT_TESTS[0], + INVALID_ELEMENT_TESTS[6], + KEY_NOT_STRING_TESTS[0], + KEY_NOT_STRING_TESTS[8], + MIXED_FORMAT_TESTS[0], ] @@ -421,9 +408,7 @@ def test_arrayToObject_literal(collection, test): ) -# --------------------------------------------------------------------------- -# Error: wrong arity -# --------------------------------------------------------------------------- +# Property [Arity]: $arrayToObject requires exactly one argument. ARITY_ERROR_TESTS = [ pytest.param({"$arrayToObject": [[], []]}, id="two_args"), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py index 2489e82e3..d6753e282 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py @@ -16,123 +16,110 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- -# Field path lookups -# --------------------------------------------------------------------------- +# Property [Field Path]: $arrayToObject resolves a field-path array argument. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_field_path", expression={"$arrayToObject": "$a.b"}, doc={"a": {"b": [{"k": "x", "v": 1}]}}, expected={"x": 1}, - msg="Should resolve nested field path", + msg="$arrayToObject should resolve nested field path", ), ExpressionTestCase( id="nonexistent_field_null", expression={"$arrayToObject": "$a.nonexistent"}, doc={"a": {"missing": 1}}, expected=None, - msg="Non-existent field should return null", + msg="$arrayToObject should return null for a non-existent field", ), ExpressionTestCase( id="deeply_nested_field", expression={"$arrayToObject": "$a.b.c"}, doc={"a": {"b": {"c": [{"k": "x", "v": 1}]}}}, expected={"x": 1}, - msg="Should resolve deeply nested field path", + msg="$arrayToObject should resolve deeply nested field path", ), ] -# --------------------------------------------------------------------------- -# Composite array path -# --------------------------------------------------------------------------- +# Property [Composite Path]: $arrayToObject resolves a composite array built from a dotted path. COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="composite_array_path", expression={"$arrayToObject": "$a.b"}, doc={"a": [{"b": {"k": "x", "v": 1}}, {"b": {"k": "y", "v": 2}}]}, expected={"x": 1, "y": 2}, - msg="Composite array path should resolve to valid k/v array", + msg="$arrayToObject should resolve a composite array path to a valid k/v array", ), ] -# --------------------------------------------------------------------------- -# Key edge cases -# --------------------------------------------------------------------------- +# Property [Key Characters]: $arrayToObject preserves special key characters from expression input. KEY_EDGE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="empty_string_key", expression={"$arrayToObject": "$arr"}, doc={"arr": [{"k": "", "v": 1}]}, expected={"": 1}, - msg="Should handle empty string key", + msg="$arrayToObject should handle empty string key", ), ExpressionTestCase( id="key_with_dots", expression={"$arrayToObject": "$arr"}, doc={"arr": [{"k": "a.b.c", "v": 1}]}, expected={"a.b.c": 1}, - msg="Should handle key with dots", + msg="$arrayToObject should handle key with dots", ), ExpressionTestCase( id="key_with_dollar", expression={"$arrayToObject": "$arr"}, doc={"arr": [{"k": "$field", "v": 1}]}, expected={"$field": 1}, - msg="Should handle key with dollar sign", + msg="$arrayToObject should handle key with dollar sign", ), ] -# --------------------------------------------------------------------------- -# $let and system variables -# --------------------------------------------------------------------------- +# Property [Variables]: $arrayToObject works with $let and system variables like $$ROOT. SYSTEM_VAR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="let_variable", expression={"$let": {"vars": {"arr": "$arr"}, "in": {"$arrayToObject": "$$arr"}}}, doc={"arr": [["a", 1]]}, expected={"a": 1}, - msg="Should work with $let variable", + msg="$arrayToObject should work with $let variable", ), ExpressionTestCase( id="root_variable", expression={"$arrayToObject": "$$ROOT.pairs"}, doc={"_id": 1, "pairs": [["a", 1]]}, expected={"a": 1}, - msg="Should work with $$ROOT", + msg="$arrayToObject should work with $$ROOT", ), ExpressionTestCase( id="current_variable", expression={"$arrayToObject": "$$CURRENT.pairs"}, doc={"_id": 2, "pairs": [["a", 1]]}, expected={"a": 1}, - msg="$$CURRENT should be equivalent to field path", + msg="$arrayToObject should treat $$CURRENT like the field path", ), ] -# --------------------------------------------------------------------------- -# Null/missing via expression -# --------------------------------------------------------------------------- +# Property [Null Propagation]: $arrayToObject returns null when the field path is null or missing. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="missing_field", expression={"$arrayToObject": "$nonexistent"}, doc={"other": 1}, expected=None, - msg="Missing field should return null", + msg="$arrayToObject should return null for a missing field", ), ExpressionTestCase( id="missing_input_type_is_null", expression={"$type": {"$arrayToObject": "$nonexistent"}}, doc={"x": 1}, expected="null", - msg="Missing field should produce null type", + msg="$arrayToObject should produce null type for a missing field", ), ] -# --------------------------------------------------------------------------- -# Aggregate and test -# --------------------------------------------------------------------------- ALL_EXPR_TESTS = ( FIELD_LOOKUP_TESTS + COMPOSITE_PATH_TESTS diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py index 67f87c675..955a6fd08 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py @@ -34,21 +34,19 @@ INT64_MIN, ) -# --------------------------------------------------------------------------- -# BSON types preserved after concatenation -# --------------------------------------------------------------------------- +# Property [Type Preservation]: $concatArrays preserves each element's BSON type. BSON_TYPE_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="int64_values", arrays=[[Int64(1), Int64(2)], [Int64(3)]], expected=[Int64(1), Int64(2), Int64(3)], - msg="Should preserve Int64 values", + msg="$concatArrays should preserve Int64 values", ), ConcatArraysTest( id="decimal128_values", arrays=[[Decimal128("1.5")], [Decimal128("2.5"), Decimal128("3.5")]], expected=[Decimal128("1.5"), Decimal128("2.5"), Decimal128("3.5")], - msg="Should preserve Decimal128 values", + msg="$concatArrays should preserve Decimal128 values", ), ConcatArraysTest( id="datetime_values", @@ -60,7 +58,7 @@ datetime(2024, 1, 1, tzinfo=timezone.utc), datetime(2024, 6, 1, tzinfo=timezone.utc), ], - msg="Should preserve datetime values", + msg="$concatArrays should preserve datetime values", ), ConcatArraysTest( id="objectid_values", @@ -72,31 +70,31 @@ ObjectId("000000000000000000000001"), ObjectId("000000000000000000000002"), ], - msg="Should preserve ObjectId values", + msg="$concatArrays should preserve ObjectId values", ), ConcatArraysTest( id="binary_values", arrays=[[Binary(b"\x01", 0)], [Binary(b"\x02", 0)]], expected=[b"\x01", b"\x02"], - msg="Should preserve Binary values", + msg="$concatArrays should preserve Binary values", ), ConcatArraysTest( id="regex_values", arrays=[[Regex("^a", "i")], [Regex("^b", "i")]], expected=[Regex("^a", "i"), Regex("^b", "i")], - msg="Should preserve Regex values", + msg="$concatArrays should preserve Regex values", ), ConcatArraysTest( id="timestamp_values", arrays=[[Timestamp(1, 0)], [Timestamp(2, 0)]], expected=[Timestamp(1, 0), Timestamp(2, 0)], - msg="Should preserve Timestamp values", + msg="$concatArrays should preserve Timestamp values", ), ConcatArraysTest( id="minkey_maxkey", arrays=[[MinKey()], [MaxKey()]], expected=[MinKey(), MaxKey()], - msg="Should preserve MinKey/MaxKey values", + msg="$concatArrays should preserve MinKey/MaxKey values", ), ConcatArraysTest( id="uuid_values", @@ -108,19 +106,17 @@ 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="$concatArrays should preserve UUID binary values", ), ] -# --------------------------------------------------------------------------- -# Mixed BSON types across arrays -# --------------------------------------------------------------------------- +# Property [Mixed Types]: $concatArrays concatenates arrays holding mixed BSON element types. MIXED_BSON_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="mixed_bson_types", arrays=[[1, "two", Int64(3)], [Decimal128("4"), True, None, MinKey()]], expected=[1, "two", Int64(3), Decimal128("4"), True, None, MinKey()], - msg="Should concatenate mixed BSON types preserving each", + msg="$concatArrays should concatenate mixed BSON types preserving each", ), ConcatArraysTest( id="mixed_dates_and_ids", @@ -134,67 +130,60 @@ Timestamp(1, 0), b"\x01", ], - msg="Should concatenate dates, ObjectIds, timestamps, and binary", + msg="$concatArrays should concatenate dates, ObjectIds, timestamps, and binary", ), ConcatArraysTest( id="mixed_extremes", arrays=[[MinKey(), FLOAT_NEGATIVE_INFINITY, None], [FLOAT_INFINITY, MaxKey()]], expected=[MinKey(), FLOAT_NEGATIVE_INFINITY, None, FLOAT_INFINITY, MaxKey()], - msg="Should concatenate MinKey, MaxKey, infinities, and null", + msg="$concatArrays should concatenate MinKey, MaxKey, infinities, and null", ), ] -# --------------------------------------------------------------------------- -# Special numeric values as elements -# --------------------------------------------------------------------------- +# Property [Special Numerics]: $concatArrays preserves NaN, Infinity, and negative zero elements. SPECIAL_NUMERIC_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="infinity_values", arrays=[[FLOAT_INFINITY], [FLOAT_NEGATIVE_INFINITY]], expected=[FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY], - msg="Should preserve infinity values", + msg="$concatArrays should preserve infinity values", ), ConcatArraysTest( id="decimal128_infinity", arrays=[[DECIMAL128_INFINITY], [DECIMAL128_NEGATIVE_INFINITY]], expected=[DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY], - msg="Should preserve Decimal128 infinity values", + msg="$concatArrays should preserve Decimal128 infinity values", ), ConcatArraysTest( id="boundary_values", arrays=[[INT32_MIN, INT32_MAX], [INT64_MIN, INT64_MAX]], expected=[INT32_MIN, INT32_MAX, INT64_MIN, INT64_MAX], - msg="Should preserve numeric boundary values", + msg="$concatArrays should preserve numeric boundary values", ), ConcatArraysTest( id="negative_zero", arrays=[[DOUBLE_NEGATIVE_ZERO], [DECIMAL128_NEGATIVE_ZERO]], expected=[DOUBLE_NEGATIVE_ZERO, DECIMAL128_NEGATIVE_ZERO], - msg="Should preserve negative zero values", + msg="$concatArrays should preserve negative zero values", ), ] -# --------------------------------------------------------------------------- -# Element identity preservation -# --------------------------------------------------------------------------- +# Property [Element Identity]: $concatArrays preserves element values and order. ELEMENT_PRESERVATION_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="decimal128_trailing_zeros", arrays=[[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="$concatArrays should preserve Decimal128 trailing zeros", ), ConcatArraysTest( id="decimal128_nan", arrays=[[DECIMAL128_NAN], [Decimal128("1")]], expected=[DECIMAL128_NAN, Decimal128("1")], - msg="Decimal128 NaN preserved", + msg="$concatArrays should preserve a Decimal128 NaN element", ), ] -# --------------------------------------------------------------------------- -# Aggregate and test -# --------------------------------------------------------------------------- ALL_BSON_TESTS = ( BSON_TYPE_TESTS + MIXED_BSON_TESTS + SPECIAL_NUMERIC_TESTS + ELEMENT_PRESERVATION_TESTS ) @@ -212,10 +201,10 @@ def test_concatArrays_bson_insert(collection, test): 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 + BSON_TYPE_TESTS[0], + BSON_TYPE_TESTS[4], + MIXED_BSON_TESTS[0], + SPECIAL_NUMERIC_TESTS[0], ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_core_behavior.py index 30d453b9b..548d634f8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_core_behavior.py @@ -18,207 +18,191 @@ ) from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- -# Success: basic concatenation -# --------------------------------------------------------------------------- +# Property [Concatenation]: $concatArrays joins multiple arrays into one in argument order. BASIC_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="two_int_arrays", arrays=[[1, 2], [3, 4]], expected=[1, 2, 3, 4], - msg="Should concatenate two int arrays", + msg="$concatArrays should concatenate two int arrays", ), ConcatArraysTest( id="two_string_arrays", arrays=[["a", "b"], ["c", "d"]], expected=["a", "b", "c", "d"], - msg="Should concatenate two string arrays", + msg="$concatArrays should concatenate two string arrays", ), ConcatArraysTest( id="three_arrays", arrays=[[1, 2], [3, 4], [5, 6]], expected=[1, 2, 3, 4, 5, 6], - msg="Should concatenate three arrays", + msg="$concatArrays should concatenate three arrays", ), ConcatArraysTest( id="mixed_type_elements", arrays=[[1, "two"], [True, None, {"a": 1}]], expected=[1, "two", True, None, {"a": 1}], - msg="Should concatenate arrays with mixed types", + msg="$concatArrays should concatenate arrays with mixed types", ), ] -# --------------------------------------------------------------------------- -# Success: empty arrays -# --------------------------------------------------------------------------- +# Property [Empty Arrays]: $concatArrays treats empty arrays as contributing no elements. EMPTY_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="both_empty", arrays=[[], []], expected=[], - msg="Should return empty array for two empty arrays", + msg="$concatArrays should return empty array for two empty arrays", ), ConcatArraysTest( id="first_empty", arrays=[[], [1, 2]], expected=[1, 2], - msg="Should return second array when first is empty", + msg="$concatArrays should return second array when first is empty", ), ConcatArraysTest( id="second_empty", arrays=[[1, 2], []], expected=[1, 2], - msg="Should return first array when second is empty", + msg="$concatArrays should return first array when second is empty", ), ConcatArraysTest( id="all_empty", arrays=[[], [], []], expected=[], - msg="Should return empty array for all empty inputs", + msg="$concatArrays should return empty array for all empty inputs", ), ConcatArraysTest( id="no_arguments", arrays=[], expected=[], - msg="No arguments returns []", + msg="$concatArrays should return an empty array for no arguments", ), ConcatArraysTest( id="empty_between_nonempty", arrays=[[1], [], [2]], expected=[1, 2], - msg="Empty between non-empty is skipped", + msg="$concatArrays should skip an empty array between non-empty arrays", ), ConcatArraysTest( id="multiple_empty", arrays=[[], [], [], []], expected=[], - msg="Multiple empty arrays returns []", + msg="$concatArrays should return an empty array for multiple empty arrays", ), ] -# --------------------------------------------------------------------------- -# Success: single array -# --------------------------------------------------------------------------- +# Property [Single Array]: $concatArrays returns a single array argument unchanged. SINGLE_ARRAY_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="single_array", arrays=[[1, 2, 3]], expected=[1, 2, 3], - msg="Should return the single array unchanged", + msg="$concatArrays should return the single array unchanged", ), ConcatArraysTest( id="single_empty_array", arrays=[[]], expected=[], - msg="Should return empty array for single empty input", + msg="$concatArrays should return empty array for single empty input", ), ] -# --------------------------------------------------------------------------- -# Success: nested arrays — top-level only -# --------------------------------------------------------------------------- +# Property [Top Level Only]: $concatArrays joins at the top level without flattening. NESTED_ARRAY_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="nested_subarrays", arrays=[[[1, 2]], [[3, 4]]], expected=[[1, 2], [3, 4]], - msg="Should concatenate top-level, not flatten subarrays", + msg="$concatArrays should concatenate top-level, not flatten subarrays", ), ConcatArraysTest( id="mixed_nested", arrays=[[[1], "two"], [[3, 4]]], expected=[[1], "two", [3, 4]], - msg="Should concatenate mixed nested elements", + msg="$concatArrays should concatenate mixed nested elements", ), ConcatArraysTest( id="deeply_nested", arrays=[[[[1]]], [[[2]]]], expected=[[[1]], [[2]]], - msg="Deep nesting preserved", + msg="$concatArrays should preserve deeply nested array elements", ), ConcatArraysTest( id="empty_nested", arrays=[[[]], [[]]], expected=[[], []], - msg="Empty nested arrays preserved", + msg="$concatArrays should preserve empty nested arrays as elements", ), ] -# --------------------------------------------------------------------------- -# Success: duplicates -# --------------------------------------------------------------------------- +# Property [Duplicates]: $concatArrays keeps duplicate elements from the inputs. DUPLICATE_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="duplicate_elements", arrays=[[1, 2, 3], [2, 3, 4]], expected=[1, 2, 3, 2, 3, 4], - msg="Should preserve duplicate elements across arrays", + msg="$concatArrays should preserve duplicate elements across arrays", ), ConcatArraysTest( id="identical_arrays", arrays=[[1, 2], [1, 2]], expected=[1, 2, 1, 2], - msg="Should concatenate identical arrays", + msg="$concatArrays should concatenate identical arrays", ), ] -# --------------------------------------------------------------------------- -# Success: null propagation -# --------------------------------------------------------------------------- +# Property [Null Propagation]: $concatArrays returns null when any argument is null or missing. NULL_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="null_first_arg", arrays=[None, [1, 2]], expected=None, - msg="Should return null when first argument is null", + msg="$concatArrays should return null when first argument is null", ), ConcatArraysTest( id="null_second_arg", arrays=[[1, 2], None], expected=None, - msg="Should return null when second argument is null", + msg="$concatArrays should return null when second argument is null", ), ConcatArraysTest( id="all_null", arrays=[None, None], expected=None, - msg="Should return null when all arguments are null", + msg="$concatArrays should return null when all arguments are null", ), ConcatArraysTest( id="null_among_three", arrays=[[1], None, [2]], expected=None, - msg="Should return null when any argument is null", + msg="$concatArrays should return null when any argument is null", ), ConcatArraysTest( id="null_elements_in_arrays", arrays=[[1, None], [None, 2]], expected=[1, None, None, 2], - msg="Should preserve null elements within arrays", + msg="$concatArrays should preserve null elements within arrays", ), ] -# --------------------------------------------------------------------------- -# Success: arrays of objects -# --------------------------------------------------------------------------- +# Property [Object Elements]: $concatArrays concatenates arrays of documents intact. OBJECT_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="arrays_of_objects", arrays=[[{"a": 1}], [{"b": 2}]], expected=[{"a": 1}, {"b": 2}], - msg="Should concatenate arrays of objects", + msg="$concatArrays should concatenate arrays of objects", ), ConcatArraysTest( id="objects_with_arrays", arrays=[[{"items": [1, 2]}], [{"items": [3, 4]}]], expected=[{"items": [1, 2]}, {"items": [3, 4]}], - msg="Should preserve inner arrays in objects", + msg="$concatArrays should preserve inner arrays in objects", ), ] -# --------------------------------------------------------------------------- -# Success: large arrays -# --------------------------------------------------------------------------- +# Property [Large Arrays]: $concatArrays concatenates large arrays. _LARGE_A = list(range(500)) _LARGE_B = list(range(500, 1000)) @@ -227,55 +211,50 @@ id="large_arrays", arrays=[_LARGE_A, _LARGE_B], expected=list(range(1000)), - msg="Should concatenate large arrays", + msg="$concatArrays should concatenate large arrays", ), ConcatArraysTest( id="two_5000_arrays", arrays=[list(range(5000)), list(range(5000, 10000))], expected=list(range(10000)), - msg="10000 elements from two large arrays", + msg="$concatArrays should concatenate two large arrays into 10,000 elements", ), ConcatArraysTest( id="one_large_one_small", arrays=[list(range(10000)), [10000]], expected=list(range(10001)), - msg="10001 elements from large + small", + msg="$concatArrays should concatenate a large array and a small array", ), ConcatArraysTest( id="100_single_element_arrays", arrays=[[i] for i in range(100)], expected=list(range(100)), - msg="100 single-element arrays concatenated", + msg="$concatArrays should concatenate 100 single-element arrays", ), ] -# --------------------------------------------------------------------------- -# Success: many arrays -# --------------------------------------------------------------------------- +# Property [Many Arrays]: $concatArrays concatenates many array arguments. MANY_ARRAYS_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="five_arrays", arrays=[[1], [2], [3], [4], [5]], expected=[1, 2, 3, 4, 5], - msg="Should concatenate five arrays", + msg="$concatArrays should concatenate five arrays", ), ConcatArraysTest( id="ten_empty_arrays", arrays=[[] for _ in range(10)], expected=[], - msg="Should concatenate ten empty arrays", + msg="$concatArrays should concatenate ten empty arrays", ), ConcatArraysTest( id="fifty_arrays", arrays=[[i] for i in range(50)], expected=list(range(50)), - msg="50 arrays concatenated", + msg="$concatArrays should concatenate 50 arrays", ), ] -# --------------------------------------------------------------------------- -# Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( BASIC_TESTS + EMPTY_TESTS @@ -301,11 +280,11 @@ def test_concatArrays_insert(collection, test): TEST_SUBSET_FOR_LITERAL = [ - BASIC_TESTS[0], # two_int_arrays - BASIC_TESTS[2], # three_arrays - EMPTY_TESTS[0], # both_empty - SINGLE_ARRAY_TESTS[0], # single_array - NESTED_ARRAY_TESTS[0], # nested_subarrays + BASIC_TESTS[0], + BASIC_TESTS[2], + EMPTY_TESTS[0], + SINGLE_ARRAY_TESTS[0], + NESTED_ARRAY_TESTS[0], ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py index fdca74b26..cd1138663 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py @@ -6,7 +6,7 @@ non-null input. """ -from datetime import datetime +from datetime import datetime, timezone import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp @@ -38,249 +38,238 @@ INT64_MIN, ) -# --------------------------------------------------------------------------- -# Error: non-array input — standard BSON types -# --------------------------------------------------------------------------- +# Property [Array Type Strictness]: $concatArrays rejects a non-array argument. NOT_ARRAY_ERROR_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="string_input", arrays=["hello", [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject string input", + msg="$concatArrays should reject string input", ), ConcatArraysTest( id="int_input", arrays=[42, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject int input", + msg="$concatArrays should reject int input", ), ConcatArraysTest( id="negative_int_input", arrays=[-42, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject negative int input", + msg="$concatArrays should reject negative int input", ), ConcatArraysTest( id="bool_input", arrays=[True, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject bool input", + msg="$concatArrays should reject bool input", ), ConcatArraysTest( id="object_input", arrays=[{"a": 1}, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject object input", + msg="$concatArrays should reject object input", ), ConcatArraysTest( id="double_input", arrays=[3.14, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject double input", + msg="$concatArrays should reject double input", ), ConcatArraysTest( id="negative_double_input", arrays=[-3.14, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject negative double input", + msg="$concatArrays should reject negative double input", ), ConcatArraysTest( id="decimal128_input", arrays=[Decimal128("1"), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject decimal128 input", + msg="$concatArrays should reject decimal128 input", ), ConcatArraysTest( id="int64_input", arrays=[Int64(1), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject int64 input", + msg="$concatArrays should reject int64 input", ), ConcatArraysTest( id="objectid_input", arrays=[ObjectId(), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject objectid input", + msg="$concatArrays should reject objectid input", ), ConcatArraysTest( id="datetime_input", - arrays=[datetime(2024, 1, 1), [1]], + arrays=[datetime(2024, 1, 1, tzinfo=timezone.utc), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject datetime input", + msg="$concatArrays should reject datetime input", ), ConcatArraysTest( id="binary_input", arrays=[Binary(b"x", 0), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject binary input", + msg="$concatArrays should reject binary input", ), ConcatArraysTest( id="regex_input", arrays=[Regex("x"), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject regex input", + msg="$concatArrays should reject regex input", ), ConcatArraysTest( id="maxkey_input", arrays=[MaxKey(), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject maxkey input", + msg="$concatArrays should reject maxkey input", ), ConcatArraysTest( id="minkey_input", arrays=[MinKey(), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject minkey input", + msg="$concatArrays should reject minkey input", ), ConcatArraysTest( id="timestamp_input", arrays=[Timestamp(0, 0), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject timestamp input", + msg="$concatArrays should reject timestamp input", ), ConcatArraysTest( id="non_array_second_arg", arrays=[[1], 42], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject non-array in second position", + msg="$concatArrays should reject non-array in second position", ), ConcatArraysTest( id="non_array_middle_arg", arrays=[[1], "bad", [2]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject non-array in middle position", + msg="$concatArrays should reject non-array in middle position", ), ] -# --------------------------------------------------------------------------- -# Error: special float/Decimal128 values -# --------------------------------------------------------------------------- +# Property [Non-Array Numerics]: $concatArrays rejects special float/Decimal128 arguments. SPECIAL_NUMERIC_ERROR_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="nan_input", arrays=[FLOAT_NAN, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject NaN input", + msg="$concatArrays should reject NaN input", ), ConcatArraysTest( id="inf_input", arrays=[FLOAT_INFINITY, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject Infinity input", + msg="$concatArrays should reject Infinity input", ), ConcatArraysTest( id="neg_inf_input", arrays=[FLOAT_NEGATIVE_INFINITY, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject -Infinity input", + msg="$concatArrays should reject -Infinity input", ), ConcatArraysTest( id="neg_zero_input", arrays=[DOUBLE_NEGATIVE_ZERO, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject negative zero input", + msg="$concatArrays should reject negative zero input", ), ConcatArraysTest( id="decimal128_nan_input", arrays=[DECIMAL128_NAN, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject Decimal128 NaN input", + msg="$concatArrays should reject Decimal128 NaN input", ), ConcatArraysTest( id="decimal128_neg_nan_input", arrays=[Decimal128("-NaN"), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject Decimal128 -NaN input", + msg="$concatArrays should reject Decimal128 -NaN input", ), ConcatArraysTest( id="decimal128_inf_input", arrays=[DECIMAL128_INFINITY, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject Decimal128 Infinity input", + msg="$concatArrays should reject Decimal128 Infinity input", ), ConcatArraysTest( id="decimal128_neg_inf_input", arrays=[DECIMAL128_NEGATIVE_INFINITY, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject Decimal128 -Infinity input", + msg="$concatArrays should reject Decimal128 -Infinity input", ), ConcatArraysTest( id="decimal128_neg_zero_input", arrays=[DECIMAL128_NEGATIVE_ZERO, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject Decimal128 -0 input", + msg="$concatArrays should reject Decimal128 -0 input", ), ] -# --------------------------------------------------------------------------- -# Error: numeric boundary values -# --------------------------------------------------------------------------- +# Property [Non-Array Boundaries]: $concatArrays rejects numeric boundary values as arguments. BOUNDARY_ERROR_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="int32_max_input", arrays=[INT32_MAX, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject INT32_MAX input", + msg="$concatArrays should reject INT32_MAX input", ), ConcatArraysTest( id="int32_min_input", arrays=[INT32_MIN, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject INT32_MIN input", + msg="$concatArrays should reject INT32_MIN input", ), ConcatArraysTest( id="int64_max_input", arrays=[INT64_MAX, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject INT64_MAX input", + msg="$concatArrays should reject INT64_MAX input", ), ConcatArraysTest( id="int64_min_input", arrays=[INT64_MIN, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject INT64_MIN input", + msg="$concatArrays should reject INT64_MIN input", ), ConcatArraysTest( id="decimal128_max_input", arrays=[DECIMAL128_MAX, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject DECIMAL128_MAX input", + msg="$concatArrays should reject DECIMAL128_MAX input", ), ConcatArraysTest( id="decimal128_min_input", arrays=[DECIMAL128_MIN, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject DECIMAL128_MIN input", + msg="$concatArrays should reject DECIMAL128_MIN input", ), ] -# --------------------------------------------------------------------------- -# Error: string edge cases -# --------------------------------------------------------------------------- +# Property [Non-Array Strings]: $concatArrays rejects string arguments regardless of content. STRING_EDGE_ERROR_TESTS: list[ConcatArraysTest] = [ ConcatArraysTest( id="comma_separated_string_input", arrays=["1, 2, 3", [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject comma-separated string", + msg="$concatArrays should reject comma-separated string", ), ConcatArraysTest( id="json_like_string_input", arrays=["[1, 2, 3]", [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject JSON-like string", + msg="$concatArrays should reject JSON-like string", ), ConcatArraysTest( id="empty_object_input", arrays=[{}, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Should reject empty object as arg", + msg="$concatArrays should reject empty object as arg", ), ] -# --------------------------------------------------------------------------- -# Aggregate and test -# --------------------------------------------------------------------------- ALL_TESTS = ( NOT_ARRAY_ERROR_TESTS + SPECIAL_NUMERIC_ERROR_TESTS @@ -301,10 +290,10 @@ def test_concatArrays_not_array_insert(collection, test): TEST_SUBSET_FOR_LITERAL = [ - NOT_ARRAY_ERROR_TESTS[0], # string_input - NOT_ARRAY_ERROR_TESTS[-3], # timestamp_input - SPECIAL_NUMERIC_ERROR_TESTS[0], # nan_input - BOUNDARY_ERROR_TESTS[0], # int32_max_input + NOT_ARRAY_ERROR_TESTS[0], + NOT_ARRAY_ERROR_TESTS[-3], + SPECIAL_NUMERIC_ERROR_TESTS[0], + BOUNDARY_ERROR_TESTS[0], ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py index 204db7f0b..04dd86a55 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py @@ -17,77 +17,71 @@ from documentdb_tests.framework.error_codes import CONCAT_ARRAYS_NOT_ARRAY_ERROR from documentdb_tests.framework.parametrize import pytest_params -# --------------------------------------------------------------------------- -# Field path lookups -# --------------------------------------------------------------------------- +# Property [Field Path]: $concatArrays resolves field-path array arguments. FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_field_path", expression={"$concatArrays": ["$a.b", "$a.c"]}, doc={"a": {"b": [1, 2], "c": [3, 4]}}, expected=[1, 2, 3, 4], - msg="Should resolve nested field paths", + msg="$concatArrays should resolve nested field paths", ), ExpressionTestCase( id="deeply_nested_field", expression={"$concatArrays": ["$a.b.c", "$a.b.d"]}, doc={"a": {"b": {"c": [10], "d": [20]}}}, expected=[10, 20], - msg="Should resolve deeply nested field paths", + msg="$concatArrays should resolve deeply nested field paths", ), ExpressionTestCase( id="nonexistent_field_null", expression={"$concatArrays": ["$a.nonexistent", "$b"]}, doc={"a": {"missing": 1}, "b": [1]}, expected=None, - msg="Non-existent field should propagate null", + msg="$concatArrays should propagate null for a non-existent field", ), ExpressionTestCase( id="array_index_path", expression={"$concatArrays": ["$a.0", [5]]}, doc={"a": [[1, 2], [3, 4]]}, expected=[5], - msg="$a.0 resolves to [] in expression context", + msg="$concatArrays should resolve $a.0 to an empty array in expression context", ), ExpressionTestCase( id="nonexistent_nested_path_empty", expression={"$concatArrays": ["$f.x", [3]]}, doc={"f": [{"g": 1}, {"g": 2}]}, expected=[3], - msg="Non-existent nested path resolves to empty array", + msg="$concatArrays should resolve a non-existent nested path to an empty array", ), ExpressionTestCase( id="nested_array_of_object_path", expression={"$concatArrays": ["$a.b.c", [3]]}, doc={"a": {"b": [{"c": [1]}, {"c": [2]}]}}, expected=[[1], [2], 3], - msg="Deep nested path resolved", + msg="$concatArrays should resolve a deeply nested field path", ), ] -# --------------------------------------------------------------------------- -# Composite array paths -# --------------------------------------------------------------------------- +# Property [Composite Path]: $concatArrays resolves composite arrays from dotted paths. COMPOSITE_PATH_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="composite_array", expression={"$concatArrays": ["$x.y", [100]]}, doc={"x": [{"y": 10}, {"y": 20}]}, expected=[10, 20, 100], - msg="Composite array path from array-of-objects", + msg="$concatArrays should resolve a composite array path from an array of objects", ), ExpressionTestCase( id="composite_path_tags", expression={"$concatArrays": ["$items.tags", ["d"]]}, doc={"items": [{"tags": ["a", "b"]}, {"tags": ["c"]}]}, expected=[["a", "b"], ["c"], "d"], - msg="$items.tags resolves to array of arrays", + msg="$concatArrays should resolve $items.tags to an array of arrays", ), ] -# --------------------------------------------------------------------------- -# $let and system variables -# --------------------------------------------------------------------------- +# Property [Variables]: $concatArrays works with $let and system variables like $$ROOT. LET_AND_VARIABLE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="let_variable", @@ -99,21 +93,21 @@ }, doc={"arr1": [1, 2], "arr2": [3, 4]}, expected=[1, 2, 3, 4], - msg="Should work with $let variables", + msg="$concatArrays should work with $let variables", ), ExpressionTestCase( id="root_variable", expression={"$concatArrays": ["$$ROOT.a", "$$ROOT.b"]}, doc={"_id": 1, "a": [1], "b": [2]}, expected=[1, 2], - msg="Should work with $$ROOT", + msg="$concatArrays should work with $$ROOT", ), ExpressionTestCase( id="current_variable", expression={"$concatArrays": ["$$CURRENT.a", "$$CURRENT.b"]}, doc={"_id": 2, "a": [1], "b": [2]}, expected=[1, 2], - msg="$$CURRENT should be equivalent to field path", + msg="$concatArrays should treat $$CURRENT like the field path", ), ExpressionTestCase( id="let_null_variable", @@ -125,110 +119,106 @@ }, doc={"_placeholder": 1}, expected=None, - msg="$let null variable returns null", + msg="$concatArrays should return null for a null $let variable", ), ] -# --------------------------------------------------------------------------- -# Null/missing via expression -# --------------------------------------------------------------------------- +# Property [Null Propagation]: $concatArrays returns null when a field path is null or missing. NULL_MISSING_EXPR_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="missing_field", expression={"$concatArrays": ["$nonexistent", [1]]}, doc={"other": 1}, expected=None, - msg="Missing field should propagate null", + msg="$concatArrays should propagate null for a missing field", ), ExpressionTestCase( id="missing_input_type_is_null", expression={"$type": {"$concatArrays": ["$nonexistent", [1]]}}, doc={"x": 1}, expected="null", - msg="Missing field should produce null type", + msg="$concatArrays should produce null type for a missing field", ), ExpressionTestCase( id="remove_variable", expression={"$concatArrays": ["$$REMOVE", [1]]}, doc={"x": 1}, expected=None, - msg="$$REMOVE propagates null", + msg="$concatArrays should return null when an argument is $$REMOVE", ), ExpressionTestCase( id="field_ref_wrapped_non_array", expression={"$concatArrays": ["$a", [1]]}, doc={"a": 1}, error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Field resolving to non-array should error", + msg="$concatArrays should error when a field resolves to a non-array", ), ExpressionTestCase( id="missing_first_field", expression={"$concatArrays": ["$a", "$b"]}, doc={"b": [1]}, expected=None, - msg="Missing first field returns null", + msg="$concatArrays should return null when the first field is missing", ), ExpressionTestCase( id="missing_last_field", expression={"$concatArrays": ["$a", "$b"]}, doc={"a": [1]}, expected=None, - msg="Missing last field returns null", + msg="$concatArrays should return null when the last field is missing", ), ExpressionTestCase( id="missing_middle_field", expression={"$concatArrays": ["$a", "$b", "$c"]}, doc={"a": [1], "c": [3]}, expected=None, - msg="Missing middle field returns null", + msg="$concatArrays should return null when a middle field is missing", ), ExpressionTestCase( id="all_missing_fields", expression={"$concatArrays": ["$a", "$b"]}, doc={"_placeholder": 1}, expected=None, - msg="All missing fields returns null", + msg="$concatArrays should return null when all fields are missing", ), ExpressionTestCase( id="explicit_null_field", expression={"$concatArrays": ["$a", "$b"]}, doc={"a": None, "b": [1]}, expected=None, - msg="Explicit null field returns null", + msg="$concatArrays should return null for an explicit null field", ), ExpressionTestCase( id="missing_plus_null", expression={"$concatArrays": ["$not_a_field", "$null_val"]}, doc={"null_val": None}, expected=None, - msg="Missing + null returns null", + msg="$concatArrays should return null for a missing field plus null", ), ExpressionTestCase( id="null_precedes_non_array", expression={"$concatArrays": ["$arr", "$null_val", "$int_val"]}, doc={"arr": [1, 2], "null_val": None, "int_val": 42}, expected=None, - msg="Null precedes non-array type error", + msg="$concatArrays should error when a null precedes a non-array argument", ), ExpressionTestCase( id="null_result_type_is_null", expression={"$type": {"$concatArrays": ["$a", "$nonexistent"]}}, doc={"a": [1]}, expected="null", - msg="$type returns 'null' not 'missing'", + msg="$concatArrays should produce null type, not missing, for a null result", ), ] -# --------------------------------------------------------------------------- -# Self-composition -# --------------------------------------------------------------------------- +# Property [Nesting]: $concatArrays can be nested as an argument to itself. SELF_COMPOSITION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="nested_concatArrays", expression={"$concatArrays": [{"$concatArrays": ["$a", "$b"]}, "$c"]}, doc={"a": [1], "b": [2], "c": [3]}, expected=[1, 2, 3], - msg="Nested $concatArrays should work", + msg="$concatArrays should evaluate a nested $concatArrays argument", ), ExpressionTestCase( id="double_nested_concatArrays", @@ -237,7 +227,7 @@ }, doc={"a": [1], "b": [2], "c": [3], "d": [4]}, expected=[1, 2, 3, 4], - msg="Both args are nested $concatArrays", + msg="$concatArrays should evaluate nested $concatArrays in both arguments", ), ExpressionTestCase( id="triple_depth_concatArrays", @@ -246,27 +236,25 @@ }, doc={"a": [1], "b": [2], "c": [3], "d": [4]}, expected=[1, 2, 3, 4], - msg="Triple nesting depth", + msg="$concatArrays should evaluate triple-nested $concatArrays", ), ] -# --------------------------------------------------------------------------- -# Same field referenced multiple times -# --------------------------------------------------------------------------- +# Property [Repeated Field]: $concatArrays repeats elements when the same field is referenced again. SAME_FIELD_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="same_field_twice", expression={"$concatArrays": ["$a", "$a"]}, doc={"a": [1, 2, 3]}, expected=[1, 2, 3, 1, 2, 3], - msg="Same field twice doubles elements", + msg="$concatArrays should double elements when a field is referenced twice", ), ExpressionTestCase( id="same_field_three_times", expression={"$concatArrays": ["$a", "$a", "$a"]}, doc={"a": [1]}, expected=[1, 1, 1], - msg="Same field three times triples elements", + msg="$concatArrays should triple elements when a field is referenced three times", ), ExpressionTestCase( id="self_concat_mixed_types", @@ -284,67 +272,60 @@ [1, 2], True, ], - msg="Self-concat preserves all types", + msg="$concatArrays should preserve all element types when self-concatenating", ), ] -# --------------------------------------------------------------------------- -# Array expression and object expression inputs -# --------------------------------------------------------------------------- +# Property [Expression Inputs]: $concatArrays evaluates array expressions, rejecting non-arrays. EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="array_expression_input", expression={"$concatArrays": [["$x", "$y"], [3]]}, doc={"x": 1, "y": 2}, expected=[1, 2, 3], - msg="Array expression with field refs resolved", + msg="$concatArrays should resolve an array expression containing field references", ), ExpressionTestCase( id="object_expression_input", expression={"$concatArrays": [{"a": "$x"}]}, doc={"x": 1}, error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="Object expression is not array", + msg="$concatArrays should reject an object expression that is not an array", ), ExpressionTestCase( id="literal_then_field", expression={"$concatArrays": [[1, 2, 3], "$a"]}, doc={"a": [1, 2]}, expected=[1, 2, 3, 1, 2], - msg="Literal + field order preserved", + msg="$concatArrays should preserve order for a literal followed by a field", ), ExpressionTestCase( id="field_then_literal", expression={"$concatArrays": ["$a", [1, 2, 3]]}, doc={"a": [1, 2]}, expected=[1, 2, 1, 2, 3], - msg="Field + literal order preserved", + msg="$concatArrays should preserve order for a field followed by a literal", ), ExpressionTestCase( id="four_fields_with_empty_and_literal", expression={"$concatArrays": ["$a", "$b", "$c", "$d", [], ["array"]]}, doc={"a": [1, 2], "b": [3, 4], "c": [5, 6], "d": []}, expected=[1, 2, 3, 4, 5, 6, "array"], - msg="Multiple fields + literals concatenated", + msg="$concatArrays should concatenate multiple fields and literals", ), ] -# --------------------------------------------------------------------------- -# Special object keys -# --------------------------------------------------------------------------- +# Property [Object Elements]: $concatArrays preserves documents with special keys as elements. SPECIAL_KEY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="special_object_keys", expression={"$concatArrays": ["$a", "$b"]}, doc={"a": [{"a.b": 1}], "b": [{"$x": 2}]}, expected=[{"a.b": 1}, {"$x": 2}], - msg="Objects with special keys preserved", + msg="$concatArrays should preserve objects with special keys", ), ] -# --------------------------------------------------------------------------- -# Aggregate and test -# --------------------------------------------------------------------------- ALL_EXPR_TESTS = ( FIELD_LOOKUP_TESTS + COMPOSITE_PATH_TESTS From 36859577e1bb3a23bc5d6a5dbe3dac5baa78a782 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 30 Jun 2026 16:03:36 -0700 Subject: [PATCH 3/9] migrate to use ArrayTestClass Signed-off-by: Alina (Xi) Li --- ...st_expression_arrayElemAt_core_behavior.py | 104 ++++---- ...st_expression_arrayElemAt_element_types.py | 82 +++---- .../test_expression_arrayElemAt_errors.py | 194 +++++++-------- ...est_expression_arrayElemAt_null_missing.py | 36 +-- ...st_expression_arrayElemAt_numeric_index.py | 90 +++---- ...st_expression_arrayElemAt_out_of_bounds.py | 58 ++--- .../arrayElemAt/utils/arrayElemAt_common.py | 19 -- ...est_expression_arrayToObject_bson_types.py | 214 ++++++++--------- ..._expression_arrayToObject_core_behavior.py | 168 ++++++------- .../test_expression_arrayToObject_errors.py | 226 +++++++++--------- .../array/arrayToObject/utils/__init__.py | 0 .../utils/arrayToObject_common.py | 15 -- ...test_expression_concatArrays_bson_types.py | 48 ++-- ...t_expression_concatArrays_core_behavior.py | 88 +++---- .../test_expression_concatArrays_errors.py | 84 +++---- .../array/concatArrays/utils/__init__.py | 0 .../concatArrays/utils/concatArrays_common.py | 15 -- .../array/{arrayElemAt => }/utils/__init__.py | 0 .../array/utils/array_test_case.py | 24 ++ 19 files changed, 720 insertions(+), 745 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/arrayElemAt_common.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/__init__.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/arrayToObject_common.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/__init__.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/concatArrays_common.py rename documentdb_tests/compatibility/tests/core/operator/expressions/array/{arrayElemAt => }/utils/__init__.py (100%) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/array_test_case.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py index cee1bfd53..a05941365 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_core_behavior.py @@ -6,8 +6,8 @@ import pytest -from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 - ArrayElemAtTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -17,73 +17,73 @@ from documentdb_tests.framework.parametrize import pytest_params # Property [Positive Index]: $arrayElemAt returns the element at the given positive index. -POSITIVE_INDEX_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +POSITIVE_INDEX_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="first_element", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=0, expected=1, msg="$arrayElemAt should return first element", ), - ArrayElemAtTest( + ArrayTestClass( id="second_element", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=1, expected=2, msg="$arrayElemAt should return second element", ), - ArrayElemAtTest( + ArrayTestClass( id="last_element", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=2, expected=3, msg="$arrayElemAt should return last element", ), - ArrayElemAtTest( + ArrayTestClass( id="single_element_array", - array=[42], + arrays=[42], idx=0, expected=42, msg="$arrayElemAt should return single element", ), - ArrayElemAtTest( + ArrayTestClass( id="string_elements", - array=["a", "b", "c"], + arrays=["a", "b", "c"], idx=1, expected="b", msg="$arrayElemAt should return string element", ), - ArrayElemAtTest( + ArrayTestClass( id="mixed_types", - array=[1, "two", 3.0, True], + arrays=[1, "two", 3.0, True], idx=2, expected=3.0, msg="$arrayElemAt should return element from mixed-type array", ), - ArrayElemAtTest( + ArrayTestClass( id="nested_array_element", - array=[[1, 2], [3, 4]], + arrays=[[1, 2], [3, 4]], idx=1, expected=[3, 4], msg="$arrayElemAt should return nested array element", ), - ArrayElemAtTest( + ArrayTestClass( id="nested_object_element", - array=[{"a": 1}, {"b": 2}], + arrays=[{"a": 1}, {"b": 2}], idx=0, expected={"a": 1}, msg="$arrayElemAt should return nested object element", ), - ArrayElemAtTest( + ArrayTestClass( id="null_element_in_array", - array=[None, 1, 2], + arrays=[None, 1, 2], idx=0, expected=None, msg="$arrayElemAt should return null element", ), - ArrayElemAtTest( + ArrayTestClass( id="bool_element", - array=[True, False], + arrays=[True, False], idx=1, expected=False, msg="$arrayElemAt should return bool element", @@ -91,31 +91,31 @@ ] # Property [Negative Index]: $arrayElemAt counts from the end of the array for a negative index. -NEGATIVE_INDEX_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +NEGATIVE_INDEX_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="last_via_neg1", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=-1, expected=3, msg="$arrayElemAt should return last element via -1", ), - ArrayElemAtTest( + ArrayTestClass( id="second_to_last", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=-2, expected=2, msg="$arrayElemAt should return second to last", ), - ArrayElemAtTest( + ArrayTestClass( id="first_via_neg_len", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=-3, expected=1, msg="$arrayElemAt should return first via negative length", ), - ArrayElemAtTest( + ArrayTestClass( id="single_element_neg1", - array=[42], + arrays=[42], idx=-1, expected=42, msg="$arrayElemAt should return single element via -1", @@ -123,24 +123,24 @@ ] # Property [Duplicate Values]: $arrayElemAt selects by position, ignoring duplicate elements. -DUPLICATE_VALUE_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +DUPLICATE_VALUE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="dup_first", - array=[1, 1, 1], + arrays=[1, 1, 1], idx=0, expected=1, msg="$arrayElemAt is unaffected by duplicate elements at index 0", ), - ArrayElemAtTest( + ArrayTestClass( id="dup_last", - array=[1, 1, 1], + arrays=[1, 1, 1], idx=2, expected=1, msg="$arrayElemAt is unaffected by duplicate elements at the last index", ), - ArrayElemAtTest( + ArrayTestClass( id="dup_neg", - array=["a", "a", "b", "a"], + arrays=["a", "a", "b", "a"], idx=-1, expected="a", msg="$arrayElemAt is unaffected by duplicate elements at a negative index", @@ -151,38 +151,38 @@ _LARGE_ARRAY_SIZE = 20_000 _LARGE_ARRAY = list(range(_LARGE_ARRAY_SIZE)) -LARGE_ARRAY_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +LARGE_ARRAY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="large_array_first", - array=_LARGE_ARRAY, + arrays=_LARGE_ARRAY, idx=0, expected=0, msg="$arrayElemAt should return first in large array", ), - ArrayElemAtTest( + ArrayTestClass( id="large_array_last", - array=_LARGE_ARRAY, + arrays=_LARGE_ARRAY, idx=_LARGE_ARRAY_SIZE - 1, expected=_LARGE_ARRAY_SIZE - 1, msg="$arrayElemAt should return last in large array", ), - ArrayElemAtTest( + ArrayTestClass( id="large_array_neg1", - array=_LARGE_ARRAY, + arrays=_LARGE_ARRAY, idx=-1, expected=_LARGE_ARRAY_SIZE - 1, msg="$arrayElemAt should return last via -1 in large array", ), - ArrayElemAtTest( + ArrayTestClass( id="large_array_middle", - array=_LARGE_ARRAY, + arrays=_LARGE_ARRAY, idx=_LARGE_ARRAY_SIZE // 2, expected=_LARGE_ARRAY_SIZE // 2, msg="$arrayElemAt should return middle in large array", ), - ArrayElemAtTest( + ArrayTestClass( id="large_array_neg_middle", - array=_LARGE_ARRAY, + arrays=_LARGE_ARRAY, idx=-(_LARGE_ARRAY_SIZE // 4), expected=_LARGE_ARRAY_SIZE - _LARGE_ARRAY_SIZE // 4, msg="$arrayElemAt should return negative middle in large array", @@ -201,7 +201,7 @@ @pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) def test_arrayElemAt_literal(collection, test): """Test $arrayElemAt with literal values.""" - result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + result = execute_expression(collection, {"$arrayElemAt": [test.arrays, test.idx]}) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) @@ -211,7 +211,7 @@ def test_arrayElemAt_literal(collection, test): def test_arrayElemAt_insert(collection, test): """Test $arrayElemAt with values from inserted documents.""" result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.arrays, "idx": test.idx} ) 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/arrayElemAt/test_expression_arrayElemAt_element_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_element_types.py index 37f6ff94d..63585578b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_element_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_element_types.py @@ -11,8 +11,8 @@ import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp -from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 - ArrayElemAtTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -32,129 +32,129 @@ ) # Property [Element Types]: $arrayElemAt returns the element with its original BSON type. -ELEMENT_TYPE_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +ELEMENT_TYPE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="int64_element", - array=[Int64(99)], + arrays=[Int64(99)], idx=0, expected=Int64(99), msg="$arrayElemAt should return Int64 element", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_element", - array=[Decimal128("1.5")], + arrays=[Decimal128("1.5")], idx=0, expected=Decimal128("1.5"), msg="$arrayElemAt should return Decimal128 element", ), - ArrayElemAtTest( + ArrayTestClass( id="datetime_element", - array=[datetime(2024, 1, 1, tzinfo=timezone.utc)], + arrays=[datetime(2024, 1, 1, tzinfo=timezone.utc)], idx=0, expected=datetime(2024, 1, 1, tzinfo=timezone.utc), msg="$arrayElemAt should return datetime element", ), - ArrayElemAtTest( + ArrayTestClass( id="binary_element", - array=[Binary(b"\x01\x02", 0)], + arrays=[Binary(b"\x01\x02", 0)], idx=0, expected=b"\x01\x02", msg="$arrayElemAt should return binary element", ), - ArrayElemAtTest( + ArrayTestClass( id="regex_element", - array=[Regex("^abc", "i")], + arrays=[Regex("^abc", "i")], idx=0, expected=Regex("^abc", "i"), msg="$arrayElemAt should return regex element", ), - ArrayElemAtTest( + ArrayTestClass( id="objectid_element", - array=[ObjectId("000000000000000000000001")], + arrays=[ObjectId("000000000000000000000001")], idx=0, expected=ObjectId("000000000000000000000001"), msg="$arrayElemAt should return ObjectId element", ), - ArrayElemAtTest( + ArrayTestClass( id="minkey_element", - array=[MinKey(), 1], + arrays=[MinKey(), 1], idx=0, expected=MinKey(), msg="$arrayElemAt should return MinKey element", ), - ArrayElemAtTest( + ArrayTestClass( id="maxkey_element", - array=[1, MaxKey()], + arrays=[1, MaxKey()], idx=1, expected=MaxKey(), msg="$arrayElemAt should return MaxKey element", ), - ArrayElemAtTest( + ArrayTestClass( id="timestamp_element", - array=[Timestamp(0, 0)], + arrays=[Timestamp(0, 0)], idx=0, expected=Timestamp(0, 0), msg="$arrayElemAt should return Timestamp element", ), - ArrayElemAtTest( + ArrayTestClass( id="float_nan_element", - array=[FLOAT_NAN, 1], + arrays=[FLOAT_NAN, 1], idx=0, expected=pytest.approx(math.nan, nan_ok=True), msg="$arrayElemAt should return NaN element", ), - ArrayElemAtTest( + ArrayTestClass( id="float_infinity_element", - array=[FLOAT_INFINITY, 1], + arrays=[FLOAT_INFINITY, 1], idx=0, expected=FLOAT_INFINITY, msg="$arrayElemAt should return Infinity element", ), - ArrayElemAtTest( + ArrayTestClass( id="float_neg_infinity_element", - array=[FLOAT_NEGATIVE_INFINITY, 1], + arrays=[FLOAT_NEGATIVE_INFINITY, 1], idx=0, expected=FLOAT_NEGATIVE_INFINITY, msg="$arrayElemAt should return -Infinity element", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_nan_element", - array=[DECIMAL128_NAN, 1], + arrays=[DECIMAL128_NAN, 1], idx=0, expected=DECIMAL128_NAN, msg="$arrayElemAt should return Decimal128 NaN element", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_infinity_element", - array=[DECIMAL128_INFINITY, 1], + arrays=[DECIMAL128_INFINITY, 1], idx=0, expected=DECIMAL128_INFINITY, msg="$arrayElemAt should return Decimal128 Infinity element", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_neg_infinity_element", - array=[DECIMAL128_NEGATIVE_INFINITY, 1], + arrays=[DECIMAL128_NEGATIVE_INFINITY, 1], idx=0, expected=DECIMAL128_NEGATIVE_INFINITY, msg="$arrayElemAt should return Decimal128 -Infinity element", ), - ArrayElemAtTest( + ArrayTestClass( id="int32_max_element", - array=[INT32_MAX, 0], + arrays=[INT32_MAX, 0], idx=0, expected=INT32_MAX, msg="$arrayElemAt should return INT32_MAX element", ), - ArrayElemAtTest( + ArrayTestClass( id="int64_max_element", - array=[INT64_MAX, 0], + arrays=[INT64_MAX, 0], idx=0, expected=INT64_MAX, msg="$arrayElemAt should return INT64_MAX element", ), - ArrayElemAtTest( + ArrayTestClass( id="mixed_special_last", - array=[INT32_MAX, FLOAT_INFINITY, DECIMAL128_NAN], + arrays=[INT32_MAX, FLOAT_INFINITY, DECIMAL128_NAN], idx=2, expected=DECIMAL128_NAN, msg="$arrayElemAt should return element from mixed special values array", @@ -171,7 +171,7 @@ @pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) def test_arrayElemAt_literal(collection, test): """Test $arrayElemAt element type preservation with literal values.""" - result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + result = execute_expression(collection, {"$arrayElemAt": [test.arrays, test.idx]}) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) @@ -181,7 +181,7 @@ def test_arrayElemAt_literal(collection, test): def test_arrayElemAt_insert(collection, test): """Test $arrayElemAt element type preservation with values from inserted documents.""" result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.arrays, "idx": test.idx} ) 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/arrayElemAt/test_expression_arrayElemAt_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py index 0fda837e4..fa414001d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_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.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 - ArrayElemAtTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -28,136 +28,136 @@ from documentdb_tests.framework.test_constants import INT64_MAX, INT64_MIN # Property [Array Type Strictness]: $arrayElemAt rejects a non-array first argument. -ARRAY_TYPE_ERROR_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +ARRAY_TYPE_ERROR_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="string_as_array", - array="hello", + arrays="hello", idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject string as array", ), - ArrayElemAtTest( + ArrayTestClass( id="int_as_array", - array=42, + arrays=42, idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject int as array", ), - ArrayElemAtTest( + ArrayTestClass( id="bool_true_as_array", - array=True, + arrays=True, idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject bool true as array", ), - ArrayElemAtTest( + ArrayTestClass( id="bool_false_as_array", - array=False, + arrays=False, idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject bool false as array", ), - ArrayElemAtTest( + ArrayTestClass( id="object_as_array", - array={"a": 1}, + arrays={"a": 1}, idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject object as array", ), - ArrayElemAtTest( + ArrayTestClass( id="double_as_array", - array=3.14, + arrays=3.14, idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject double as array", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_as_array", - array=Decimal128("1"), + arrays=Decimal128("1"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject decimal128 as array", ), - ArrayElemAtTest( + ArrayTestClass( id="int64_as_array", - array=Int64(1), + arrays=Int64(1), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject int64 as array", ), - ArrayElemAtTest( + ArrayTestClass( id="binary_as_array", - array=Binary(b"x", 0), + arrays=Binary(b"x", 0), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject binary as array", ), - ArrayElemAtTest( + ArrayTestClass( id="datetime_as_array", - array=datetime(2024, 1, 1, tzinfo=timezone.utc), + arrays=datetime(2024, 1, 1, tzinfo=timezone.utc), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject datetime as array", ), - ArrayElemAtTest( + ArrayTestClass( id="objectid_as_array", - array=ObjectId(), + arrays=ObjectId(), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject objectid as array", ), - ArrayElemAtTest( + ArrayTestClass( id="regex_as_array", - array=Regex("x"), + arrays=Regex("x"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject regex as array", ), - ArrayElemAtTest( + ArrayTestClass( id="maxkey_as_array", - array=MaxKey(), + arrays=MaxKey(), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject maxkey as array", ), - ArrayElemAtTest( + ArrayTestClass( id="minkey_as_array", - array=MinKey(), + arrays=MinKey(), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject minkey as array", ), - ArrayElemAtTest( + ArrayTestClass( id="timestamp_as_array", - array=Timestamp(0, 0), + arrays=Timestamp(0, 0), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject timestamp as array", ), - ArrayElemAtTest( + ArrayTestClass( id="nan_as_array", - array=float("nan"), + arrays=float("nan"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject NaN as array", ), - ArrayElemAtTest( + ArrayTestClass( id="inf_as_array", - array=float("inf"), + arrays=float("inf"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject Infinity as array", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_nan_as_array", - array=Decimal128("NaN"), + arrays=Decimal128("NaN"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject Decimal128 NaN as array", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_inf_as_array", - array=Decimal128("Infinity"), + arrays=Decimal128("Infinity"), idx=0, error_code=ARRAY_ELEM_AT_ARRAY_TYPE_ERROR, msg="$arrayElemAt should reject Decimal128 Infinity as array", @@ -165,87 +165,87 @@ ] # Property [Index Type Strictness]: $arrayElemAt rejects a non-numeric index. -INDEX_TYPE_ERROR_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +INDEX_TYPE_ERROR_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="string_index", - array=[1, 2], + arrays=[1, 2], idx="0", error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject string index", ), - ArrayElemAtTest( + ArrayTestClass( id="bool_true_index", - array=[1, 2], + arrays=[1, 2], idx=True, error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject bool true index", ), - ArrayElemAtTest( + ArrayTestClass( id="bool_false_index", - array=[1, 2], + arrays=[1, 2], idx=False, error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject bool false index", ), - ArrayElemAtTest( + ArrayTestClass( id="array_index", - array=[1, 2], + arrays=[1, 2], idx=[0], error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject array index", ), - ArrayElemAtTest( + ArrayTestClass( id="object_index", - array=[1, 2], + arrays=[1, 2], idx={"a": 0}, error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject object index", ), - ArrayElemAtTest( + ArrayTestClass( id="objectid_index", - array=[1, 2], + arrays=[1, 2], idx=ObjectId(), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject objectid index", ), - ArrayElemAtTest( + ArrayTestClass( id="binary_index", - array=[1, 2], + arrays=[1, 2], idx=Binary(b"\x01", 0), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject binary index", ), - ArrayElemAtTest( + ArrayTestClass( id="timestamp_index", - array=[1, 2], + arrays=[1, 2], idx=Timestamp(0, 0), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject timestamp index", ), - ArrayElemAtTest( + ArrayTestClass( id="datetime_index", - array=[1, 2], + arrays=[1, 2], idx=datetime(2024, 1, 1, tzinfo=timezone.utc), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject datetime index", ), - ArrayElemAtTest( + ArrayTestClass( id="maxkey_index", - array=[1, 2], + arrays=[1, 2], idx=MaxKey(), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject maxkey index", ), - ArrayElemAtTest( + ArrayTestClass( id="minkey_index", - array=[1, 2], + arrays=[1, 2], idx=MinKey(), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject minkey index", ), - ArrayElemAtTest( + ArrayTestClass( id="regex_index", - array=[1, 2], + arrays=[1, 2], idx=Regex("x"), error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR, msg="$arrayElemAt should reject regex index", @@ -253,101 +253,101 @@ ] # Property [Integral Index]: $arrayElemAt rejects a non-integral or out-of-range numeric index. -NON_INTEGRAL_INDEX_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +NON_INTEGRAL_INDEX_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="double_fractional_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=1.5, error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject fractional double index", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_fractional_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=Decimal128("0.5"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject fractional decimal128 index", ), - ArrayElemAtTest( + ArrayTestClass( id="double_nan_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=float("nan"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject NaN index", ), - ArrayElemAtTest( + ArrayTestClass( id="double_inf_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=float("inf"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject infinity index", ), - ArrayElemAtTest( + ArrayTestClass( id="double_neg_inf_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=float("-inf"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject -infinity index", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_nan_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=Decimal128("NaN"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject decimal128 NaN index", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_inf_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=Decimal128("Infinity"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject decimal128 infinity index", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_neg_inf_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=Decimal128("-Infinity"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject decimal128 -infinity index", ), - ArrayElemAtTest( + ArrayTestClass( id="int64_max_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=INT64_MAX, error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject INT64_MAX index", ), - ArrayElemAtTest( + ArrayTestClass( id="int64_min_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=INT64_MIN, error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject INT64_MIN index", ), - ArrayElemAtTest( + ArrayTestClass( id="large_double_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=1.0e18, error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject large double index", ), - ArrayElemAtTest( + ArrayTestClass( id="large_neg_double_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=-1.0e18, error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject large negative double index", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_beyond_int32", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=Decimal128("2147483648"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject decimal128 beyond int32", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_huge", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=Decimal128("9223372036854775808"), error_code=ARRAY_ELEM_AT_INDEX_NOT_INTEGRAL_ERROR, msg="$arrayElemAt should reject huge decimal128 index", @@ -366,7 +366,7 @@ @pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) def test_arrayElemAt_literal(collection, test): """Test $arrayElemAt error cases with literal values.""" - result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + result = execute_expression(collection, {"$arrayElemAt": [test.arrays, test.idx]}) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) @@ -376,7 +376,7 @@ def test_arrayElemAt_literal(collection, test): def test_arrayElemAt_insert(collection, test): """Test $arrayElemAt error cases with values from inserted documents.""" result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.arrays, "idx": test.idx} ) 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/arrayElemAt/test_expression_arrayElemAt_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_null_missing.py index 13ae48ec4..324e5dda5 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_null_missing.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_null_missing.py @@ -6,8 +6,8 @@ import pytest -from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 - ArrayElemAtTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -18,31 +18,31 @@ from documentdb_tests.framework.test_constants import MISSING # Property [Null Propagation]: $arrayElemAt returns null when the array or index argument is null. -NULL_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +NULL_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="null_array", - array=None, + arrays=None, idx=0, expected=None, msg="$arrayElemAt should return null for null array", ), - ArrayElemAtTest( + ArrayTestClass( id="null_array_neg_idx", - array=None, + arrays=None, idx=-1, expected=None, msg="$arrayElemAt should return null for null array with negative index", ), - ArrayElemAtTest( + ArrayTestClass( id="null_index", - array=[1, 2], + arrays=[1, 2], idx=None, expected=None, msg="$arrayElemAt should return null for null index", ), - ArrayElemAtTest( + ArrayTestClass( id="both_null", - array=None, + arrays=None, idx=None, expected=None, msg="$arrayElemAt should return null when both null", @@ -50,17 +50,17 @@ ] # Property [Missing Propagation]: $arrayElemAt returns null when the array or index is missing. -LITERAL_ONLY_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +LITERAL_ONLY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="missing_array", - array=MISSING, + arrays=MISSING, idx=0, expected=None, msg="$arrayElemAt should return null for missing array", ), - ArrayElemAtTest( + ArrayTestClass( id="missing_index", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=MISSING, expected=None, msg="$arrayElemAt should return null for missing index", @@ -77,7 +77,7 @@ @pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) def test_arrayElemAt_literal(collection, test): """Test $arrayElemAt null/missing with literal values.""" - result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + result = execute_expression(collection, {"$arrayElemAt": [test.arrays, test.idx]}) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) @@ -87,7 +87,7 @@ def test_arrayElemAt_literal(collection, test): def test_arrayElemAt_insert(collection, test): """Test $arrayElemAt null with values from inserted documents.""" result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.arrays, "idx": test.idx} ) 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/arrayElemAt/test_expression_arrayElemAt_numeric_index.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py index 16fba620f..f8e621099 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py @@ -8,8 +8,8 @@ import pytest from bson import Decimal128, Int64 -from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 - ArrayElemAtTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -20,143 +20,143 @@ from documentdb_tests.framework.test_constants import DECIMAL128_NEGATIVE_ZERO, DOUBLE_NEGATIVE_ZERO # Property [Numeric Index Types]: $arrayElemAt accepts int32, int64, and integral double indexes. -NUMERIC_INDEX_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +NUMERIC_INDEX_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="int64_zero_index", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Int64(0), expected=10, msg="$arrayElemAt should accept Int64 zero index", ), - ArrayElemAtTest( + ArrayTestClass( id="int64_index", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Int64(1), expected=20, msg="$arrayElemAt should accept Int64 index", ), - ArrayElemAtTest( + ArrayTestClass( id="double_integral_index", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=2.0, expected=30, msg="$arrayElemAt should accept integral double index", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_integral_index", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("0"), expected=10, msg="$arrayElemAt should accept Decimal128 index", ), - ArrayElemAtTest( + ArrayTestClass( id="int64_negative_index", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Int64(-1), expected=30, msg="$arrayElemAt should accept negative Int64 index", ), - ArrayElemAtTest( + ArrayTestClass( id="double_negative_integral", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=-2.0, expected=20, msg="$arrayElemAt should accept negative integral double index", ), - ArrayElemAtTest( + ArrayTestClass( id="negative_zero_index", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=-0.0, expected=10, msg="$arrayElemAt should treat -0.0 as index 0", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_negative_zero_index", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=DECIMAL128_NEGATIVE_ZERO, expected=10, msg="$arrayElemAt should treat decimal128 -0 as index 0", ), - ArrayElemAtTest( + ArrayTestClass( id="double_negative_zero_const", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=DOUBLE_NEGATIVE_ZERO, expected=10, msg="$arrayElemAt should treat double -0 const as index 0", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_trailing_zero", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("1.0"), expected=20, msg="$arrayElemAt should accept decimal128 with trailing zero", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_subnormal_zero", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("0E-6176"), expected=10, msg="$arrayElemAt should accept decimal128 subnormal zero", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_20E_neg1", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("20E-1"), expected=30, msg="$arrayElemAt should accept decimal128 20E-1 as index 2", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_0_2E1", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("0.2E1"), expected=30, msg="$arrayElemAt should accept decimal128 0.2E1 as index 2", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_2E0", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("2E0"), expected=30, msg="$arrayElemAt should accept decimal128 2E0 as index 2", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_10E_neg1", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("10E-1"), expected=20, msg="$arrayElemAt should accept decimal128 10E-1 as index 1", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_negative_integral_index", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("-1"), expected=30, msg="$arrayElemAt should accept negative Decimal128 integral index", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_neg_10E_neg1", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("-10E-1"), expected=30, msg="$arrayElemAt should accept decimal128 -10E-1 as index -1", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_0E_pos3", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("0E+3"), expected=10, msg="$arrayElemAt should accept decimal128 0E+3 as index 0", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_0E_neg3", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("0E-3"), expected=10, msg="$arrayElemAt should accept decimal128 0E-3 as index 0", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_1_00000", - array=[10, 20, 30], + arrays=[10, 20, 30], idx=Decimal128("1.00000"), expected=20, msg="$arrayElemAt should accept decimal128 1.00000 as index 1", @@ -173,7 +173,7 @@ @pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) def test_arrayElemAt_literal(collection, test): """Test $arrayElemAt numeric index types with literal values.""" - result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + result = execute_expression(collection, {"$arrayElemAt": [test.arrays, test.idx]}) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) @@ -183,7 +183,7 @@ def test_arrayElemAt_literal(collection, test): def test_arrayElemAt_insert(collection, test): """Test $arrayElemAt numeric index types with values from inserted documents.""" result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.arrays, "idx": test.idx} ) 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/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py index ebace6efa..7d3022ae1 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_out_of_bounds.py @@ -8,8 +8,8 @@ import pytest from bson import Decimal128 -from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayElemAt.utils.arrayElemAt_common import ( # noqa: E501 - ArrayElemAtTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( execute_expression, @@ -20,87 +20,87 @@ from documentdb_tests.framework.test_constants import INT32_MAX, INT32_MIN # Property [Out Of Bounds]: $arrayElemAt returns no value when the index is out of bounds. -OUT_OF_BOUNDS_TESTS: list[ArrayElemAtTest] = [ - ArrayElemAtTest( +OUT_OF_BOUNDS_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="positive_oob", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=15, expected=[{}], msg="$arrayElemAt should return no result for positive OOB", ), - ArrayElemAtTest( + ArrayTestClass( id="positive_oob_by_one", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=3, expected=[{}], msg="$arrayElemAt should return no result for OOB by one", ), - ArrayElemAtTest( + ArrayTestClass( id="negative_oob", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=-4, expected=[{}], msg="$arrayElemAt should return no result for negative OOB", ), - ArrayElemAtTest( + ArrayTestClass( id="negative_oob_large", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=-100, expected=[{}], msg="$arrayElemAt should return no result for large negative OOB", ), - ArrayElemAtTest( + ArrayTestClass( id="empty_array_idx_0", - array=[], + arrays=[], idx=0, expected=[{}], msg="$arrayElemAt should return no result for empty array idx 0", ), - ArrayElemAtTest( + ArrayTestClass( id="empty_array_neg1", - array=[], + arrays=[], idx=-1, expected=[{}], msg="$arrayElemAt should return no result for empty array idx -1", ), - ArrayElemAtTest( + ArrayTestClass( id="int32_max_oob", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=INT32_MAX, expected=[{}], msg="$arrayElemAt should return no result for INT32_MAX index", ), - ArrayElemAtTest( + ArrayTestClass( id="int32_min_oob", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=INT32_MIN, expected=[{}], msg="$arrayElemAt should return no result for INT32_MIN index", ), - ArrayElemAtTest( + ArrayTestClass( id="single_element_oob_pos", - array=[42], + arrays=[42], idx=1, expected=[{}], msg="$arrayElemAt should return no result for single element OOB positive", ), - ArrayElemAtTest( + ArrayTestClass( id="single_element_oob_neg", - array=[42], + arrays=[42], idx=-2, expected=[{}], msg="$arrayElemAt should return no result for single element OOB negative", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_oob_pos", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=Decimal128("15"), expected=[{}], msg="$arrayElemAt should return no result for Decimal128 positive OOB", ), - ArrayElemAtTest( + ArrayTestClass( id="decimal128_oob_neg", - array=[1, 2, 3], + arrays=[1, 2, 3], idx=Decimal128("-100"), expected=[{}], msg="$arrayElemAt should return no result for Decimal128 negative OOB", @@ -117,7 +117,7 @@ @pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) def test_arrayElemAt_out_of_bounds_literal(collection, test): """Test $arrayElemAt returns no result when index exceeds array bounds.""" - result = execute_expression(collection, {"$arrayElemAt": [test.array, test.idx]}) + result = execute_expression(collection, {"$arrayElemAt": [test.arrays, test.idx]}) assertSuccess(result, test.expected) @@ -125,6 +125,6 @@ def test_arrayElemAt_out_of_bounds_literal(collection, test): def test_arrayElemAt_out_of_bounds_insert(collection, test): """Test $arrayElemAt out-of-bounds with inserted documents.""" result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.array, "idx": test.idx} + collection, {"$arrayElemAt": ["$arr", "$idx"]}, {"arr": test.arrays, "idx": test.idx} ) assertSuccess(result, test.expected) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/arrayElemAt_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/arrayElemAt_common.py deleted file mode 100644 index 286445c37..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/arrayElemAt_common.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Shared test infrastructure for $arrayElemAt expression tests. - -Provides the ArrayElemAtTest dataclass and common imports used across -all $arrayElemAt test files. -""" - -from dataclasses import dataclass -from typing import Any - -from documentdb_tests.framework.test_case import BaseTestCase - - -@dataclass(frozen=True) -class ArrayElemAtTest(BaseTestCase): - """Test case for $arrayElemAt operator.""" - - array: Any = None - idx: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py index 39be0ceec..c7071ea93 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_bson_types.py @@ -14,8 +14,8 @@ import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp -from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayToObject.utils.arrayToObject_common import ( # noqa: E501 - ArrayToObjectTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -40,272 +40,272 @@ ) # Property [Value Types K/V]: $arrayToObject preserves each value's BSON type in k/v form. -BSON_KV_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +BSON_KV_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="kv_int64", - array=[{"k": "a", "v": Int64(99)}], + arrays=[{"k": "a", "v": Int64(99)}], expected={"a": Int64(99)}, msg="$arrayToObject should preserve Int64 value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_decimal128", - array=[{"k": "a", "v": Decimal128("3.14")}], + arrays=[{"k": "a", "v": Decimal128("3.14")}], expected={"a": Decimal128("3.14")}, msg="$arrayToObject should preserve Decimal128 value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_datetime", - array=[{"k": "a", "v": datetime(2024, 1, 1, tzinfo=timezone.utc)}], + arrays=[{"k": "a", "v": datetime(2024, 1, 1, tzinfo=timezone.utc)}], expected={"a": datetime(2024, 1, 1, tzinfo=timezone.utc)}, msg="$arrayToObject should preserve datetime value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_objectid", - array=[{"k": "a", "v": ObjectId("000000000000000000000001")}], + arrays=[{"k": "a", "v": ObjectId("000000000000000000000001")}], expected={"a": ObjectId("000000000000000000000001")}, msg="$arrayToObject should preserve ObjectId value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_bool_false", - array=[{"k": "a", "v": False}], + arrays=[{"k": "a", "v": False}], expected={"a": False}, msg="$arrayToObject should preserve false value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_bool_true", - array=[{"k": "a", "v": True}], + arrays=[{"k": "a", "v": True}], expected={"a": True}, msg="$arrayToObject should preserve true value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_null", - array=[{"k": "a", "v": None}], + arrays=[{"k": "a", "v": None}], expected={"a": None}, msg="$arrayToObject should preserve null value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_regex", - array=[{"k": "a", "v": Regex("^abc", "i")}], + arrays=[{"k": "a", "v": Regex("^abc", "i")}], expected={"a": Regex("^abc", "i")}, msg="$arrayToObject should preserve regex value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_minkey", - array=[{"k": "a", "v": MinKey()}], + arrays=[{"k": "a", "v": MinKey()}], expected={"a": MinKey()}, msg="$arrayToObject should preserve MinKey value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_maxkey", - array=[{"k": "a", "v": MaxKey()}], + arrays=[{"k": "a", "v": MaxKey()}], expected={"a": MaxKey()}, msg="$arrayToObject should preserve MaxKey value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_binary", - array=[{"k": "a", "v": Binary(b"\x01\x02\x03", 0)}], + arrays=[{"k": "a", "v": Binary(b"\x01\x02\x03", 0)}], expected={"a": b"\x01\x02\x03"}, msg="$arrayToObject should preserve Binary value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_timestamp", - array=[{"k": "a", "v": Timestamp(1234567890, 1)}], + arrays=[{"k": "a", "v": Timestamp(1234567890, 1)}], expected={"a": Timestamp(1234567890, 1)}, msg="$arrayToObject should preserve Timestamp value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_uuid", - array=[{"k": "a", "v": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))}], + arrays=[{"k": "a", "v": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))}], expected={"a": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))}, msg="$arrayToObject should preserve UUID binary value", ), ] # Property [Value Types Pair]: $arrayToObject preserves each value's BSON type in pair form. -BSON_PAIR_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +BSON_PAIR_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="pair_int64", - array=[["a", Int64(99)]], + arrays=[["a", Int64(99)]], expected={"a": Int64(99)}, msg="$arrayToObject should preserve Int64 value (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_decimal128", - array=[["a", Decimal128("3.14")]], + arrays=[["a", Decimal128("3.14")]], expected={"a": Decimal128("3.14")}, msg="$arrayToObject should preserve Decimal128 value (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_datetime", - array=[["a", datetime(2024, 1, 1, tzinfo=timezone.utc)]], + arrays=[["a", datetime(2024, 1, 1, tzinfo=timezone.utc)]], expected={"a": datetime(2024, 1, 1, tzinfo=timezone.utc)}, msg="$arrayToObject should preserve datetime value (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_objectid", - array=[["a", ObjectId("000000000000000000000001")]], + arrays=[["a", ObjectId("000000000000000000000001")]], expected={"a": ObjectId("000000000000000000000001")}, msg="$arrayToObject should preserve ObjectId value (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_binary", - array=[["a", Binary(b"\x01\x02\x03", 0)]], + arrays=[["a", Binary(b"\x01\x02\x03", 0)]], expected={"a": b"\x01\x02\x03"}, msg="$arrayToObject should preserve Binary value (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_timestamp", - array=[["a", Timestamp(1234567890, 1)]], + arrays=[["a", Timestamp(1234567890, 1)]], expected={"a": Timestamp(1234567890, 1)}, msg="$arrayToObject should preserve Timestamp value (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_regex", - array=[["a", Regex("^abc", "i")]], + arrays=[["a", Regex("^abc", "i")]], expected={"a": Regex("^abc", "i")}, msg="$arrayToObject should preserve regex value (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_minkey", - array=[["a", MinKey()]], + arrays=[["a", MinKey()]], expected={"a": MinKey()}, msg="$arrayToObject should preserve MinKey value (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_maxkey", - array=[["a", MaxKey()]], + arrays=[["a", MaxKey()]], expected={"a": MaxKey()}, msg="$arrayToObject should preserve MaxKey value (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_uuid", - array=[["a", Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))]], + arrays=[["a", Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))]], expected={"a": Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))}, msg="$arrayToObject should preserve UUID binary value (pair form)", ), ] # Property [Special Numerics]: $arrayToObject preserves NaN, Infinity, and negative zero. -SPECIAL_NUMERIC_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +SPECIAL_NUMERIC_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="value_infinity", - array=[{"k": "a", "v": FLOAT_INFINITY}], + arrays=[{"k": "a", "v": FLOAT_INFINITY}], expected={"a": FLOAT_INFINITY}, msg="$arrayToObject should preserve Infinity value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_neg_infinity", - array=[{"k": "a", "v": FLOAT_NEGATIVE_INFINITY}], + arrays=[{"k": "a", "v": FLOAT_NEGATIVE_INFINITY}], expected={"a": FLOAT_NEGATIVE_INFINITY}, msg="$arrayToObject should preserve -Infinity value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_neg_zero", - array=[{"k": "a", "v": DOUBLE_NEGATIVE_ZERO}], + arrays=[{"k": "a", "v": DOUBLE_NEGATIVE_ZERO}], expected={"a": DOUBLE_NEGATIVE_ZERO}, msg="$arrayToObject should preserve negative zero value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_decimal128_nan", - array=[{"k": "a", "v": DECIMAL128_NAN}], + arrays=[{"k": "a", "v": DECIMAL128_NAN}], expected={"a": DECIMAL128_NAN}, msg="$arrayToObject should preserve Decimal128 NaN value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_decimal128_infinity", - array=[{"k": "a", "v": DECIMAL128_INFINITY}], + arrays=[{"k": "a", "v": DECIMAL128_INFINITY}], expected={"a": DECIMAL128_INFINITY}, msg="$arrayToObject should preserve Decimal128 Infinity value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_decimal128_neg_infinity", - array=[{"k": "a", "v": DECIMAL128_NEGATIVE_INFINITY}], + arrays=[{"k": "a", "v": DECIMAL128_NEGATIVE_INFINITY}], expected={"a": DECIMAL128_NEGATIVE_INFINITY}, msg="$arrayToObject should preserve Decimal128 -Infinity value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_decimal128_neg_zero", - array=[{"k": "a", "v": DECIMAL128_NEGATIVE_ZERO}], + arrays=[{"k": "a", "v": DECIMAL128_NEGATIVE_ZERO}], expected={"a": DECIMAL128_NEGATIVE_ZERO}, msg="$arrayToObject should preserve Decimal128 -0 value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_decimal128_high_precision", - array=[{"k": "a", "v": Decimal128("1.234567890123456789012345678901234")}], + arrays=[{"k": "a", "v": Decimal128("1.234567890123456789012345678901234")}], expected={"a": Decimal128("1.234567890123456789012345678901234")}, msg="$arrayToObject should preserve full Decimal128 precision", ), - ArrayToObjectTest( + ArrayTestClass( id="value_decimal128_zero_exponent", - array=[{"k": "a", "v": Decimal128("0E+10")}], + arrays=[{"k": "a", "v": Decimal128("0E+10")}], expected={"a": Decimal128("0E+10")}, msg="$arrayToObject should preserve Decimal128 exponent notation", ), - ArrayToObjectTest( + ArrayTestClass( id="value_decimal128_trailing_zeros", - array=[{"k": "a", "v": Decimal128("1.00000")}], + arrays=[{"k": "a", "v": Decimal128("1.00000")}], expected={"a": Decimal128("1.00000")}, msg="$arrayToObject should preserve Decimal128 trailing zeros", ), - ArrayToObjectTest( + ArrayTestClass( id="value_decimal128_subnormal_zero", - array=[{"k": "a", "v": Decimal128("0E-6176")}], + arrays=[{"k": "a", "v": Decimal128("0E-6176")}], expected={"a": Decimal128("0E-6176")}, msg="$arrayToObject should preserve Decimal128 subnormal zero", ), ] # Property [Numeric Boundaries]: $arrayToObject preserves numeric boundary values. -BOUNDARY_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +BOUNDARY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="value_int32_max", - array=[{"k": "a", "v": INT32_MAX}], + arrays=[{"k": "a", "v": INT32_MAX}], expected={"a": INT32_MAX}, msg="$arrayToObject should preserve INT32_MAX value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_int32_min", - array=[{"k": "a", "v": INT32_MIN}], + arrays=[{"k": "a", "v": INT32_MIN}], expected={"a": INT32_MIN}, msg="$arrayToObject should preserve INT32_MIN value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_int64_max", - array=[{"k": "a", "v": INT64_MAX}], + arrays=[{"k": "a", "v": INT64_MAX}], expected={"a": INT64_MAX}, msg="$arrayToObject should preserve INT64_MAX value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_int64_min", - array=[{"k": "a", "v": INT64_MIN}], + arrays=[{"k": "a", "v": INT64_MIN}], expected={"a": INT64_MIN}, msg="$arrayToObject should preserve INT64_MIN value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_decimal128_max", - array=[{"k": "a", "v": DECIMAL128_MAX}], + arrays=[{"k": "a", "v": DECIMAL128_MAX}], expected={"a": DECIMAL128_MAX}, msg="$arrayToObject should preserve DECIMAL128_MAX value", ), - ArrayToObjectTest( + ArrayTestClass( id="value_decimal128_min", - array=[{"k": "a", "v": DECIMAL128_MIN}], + arrays=[{"k": "a", "v": DECIMAL128_MIN}], expected={"a": DECIMAL128_MIN}, msg="$arrayToObject should preserve DECIMAL128_MIN value", ), ] # Property [Nested Values]: $arrayToObject preserves nested arrays and documents as values. -NESTED_BSON_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +NESTED_BSON_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="nested_bson_in_object_value", - array=[{"k": "a", "v": {"x": Int64(1), "y": Decimal128("2.5")}}], + arrays=[{"k": "a", "v": {"x": Int64(1), "y": Decimal128("2.5")}}], expected={"a": {"x": Int64(1), "y": Decimal128("2.5")}}, msg="$arrayToObject should preserve nested BSON types in object value", ), - ArrayToObjectTest( + ArrayTestClass( id="nested_bson_in_array_value", - array=[ + arrays=[ { "k": "a", "v": [ @@ -324,47 +324,47 @@ }, msg="$arrayToObject should preserve nested BSON types in array value", ), - ArrayToObjectTest( + ArrayTestClass( id="deeply_nested_bson", - array=[{"k": "a", "v": {"x": [{"y": Decimal128("1.5")}, Timestamp(0, 0)]}}], + arrays=[{"k": "a", "v": {"x": [{"y": Decimal128("1.5")}, Timestamp(0, 0)]}}], expected={"a": {"x": [{"y": Decimal128("1.5")}, Timestamp(0, 0)]}}, msg="$arrayToObject should preserve deeply nested BSON types", ), - ArrayToObjectTest( + ArrayTestClass( id="nested_array_not_interpreted_as_kv", - array=[{"k": "a", "v": [["level2", {"x": 1}]]}], + arrays=[{"k": "a", "v": [["level2", {"x": 1}]]}], expected={"a": [["level2", {"x": 1}]]}, msg="$arrayToObject should preserve nested array as value without interpreting as k/v", ), ] # Property [Duplicate Numeric Keys]: last value wins for duplicate keys of differing numeric types. -NUMERIC_EQUIVALENCE_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +NUMERIC_EQUIVALENCE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="duplicate_key_int_then_int64", - array=[{"k": "a", "v": 1}, {"k": "a", "v": Int64(2)}], + arrays=[{"k": "a", "v": 1}, {"k": "a", "v": Int64(2)}], expected={"a": Int64(2)}, msg="$arrayToObject should keep the last Int64 value for a duplicate key", ), - ArrayToObjectTest( + ArrayTestClass( id="duplicate_key_int_then_decimal128", - array=[{"k": "a", "v": 1}, {"k": "a", "v": Decimal128("2")}], + arrays=[{"k": "a", "v": 1}, {"k": "a", "v": Decimal128("2")}], expected={"a": Decimal128("2")}, msg="$arrayToObject should keep the last Decimal128 value for a duplicate key", ), - ArrayToObjectTest( + ArrayTestClass( id="duplicate_key_decimal128_then_double", - array=[{"k": "a", "v": Decimal128("1")}, {"k": "a", "v": 2.0}], + arrays=[{"k": "a", "v": Decimal128("1")}, {"k": "a", "v": 2.0}], expected={"a": 2.0}, msg="$arrayToObject should keep the last double value for a duplicate key", ), ] # Property [Mixed Types]: $arrayToObject preserves multiple mixed BSON value types in one array. -MIXED_BSON_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +MIXED_BSON_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="kv_mixed_bson_types", - array=[ + arrays=[ {"k": "int64", "v": Int64(1)}, {"k": "dec", "v": Decimal128("1.5")}, {"k": "dt", "v": datetime(2024, 1, 1, tzinfo=timezone.utc)}, @@ -411,7 +411,7 @@ @pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) def test_arrayToObject_bson_literal(collection, test): """Test $arrayToObject BSON types with literal values.""" - result = execute_expression(collection, {"$arrayToObject": {"$literal": test.array}}) + result = execute_expression(collection, {"$arrayToObject": {"$literal": test.arrays}}) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) @@ -421,7 +421,7 @@ def test_arrayToObject_bson_literal(collection, test): def test_arrayToObject_bson_insert(collection, test): """Test $arrayToObject BSON types with values from inserted documents.""" result = execute_expression_with_insert( - collection, {"$arrayToObject": "$arr"}, {"arr": test.array} + collection, {"$arrayToObject": "$arr"}, {"arr": test.arrays} ) 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/arrayToObject/test_expression_arrayToObject_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py index ff0c0d360..527ca5700 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py @@ -8,8 +8,8 @@ import pytest -from documentdb_tests.compatibility.tests.core.operator.expressions.array.arrayToObject.utils.arrayToObject_common import ( # noqa: E501 - ArrayToObjectTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -19,28 +19,28 @@ from documentdb_tests.framework.parametrize import pytest_params # Property [K/V Form]: $arrayToObject builds an object from {k, v} document entries. -KV_FORM_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +KV_FORM_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="kv_single_pair", - array=[{"k": "a", "v": 1}], + arrays=[{"k": "a", "v": 1}], expected={"a": 1}, msg="$arrayToObject should convert single k/v pair", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_multiple_pairs", - array=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "c", "v": 3}], + arrays=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "c", "v": 3}], expected={"a": 1, "b": 2, "c": 3}, msg="$arrayToObject should convert multiple k/v pairs", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_string_values", - array=[{"k": "name", "v": "Alice"}, {"k": "city", "v": "Mycity"}], + arrays=[{"k": "name", "v": "Alice"}, {"k": "city", "v": "Mycity"}], expected={"name": "Alice", "city": "Mycity"}, msg="$arrayToObject should convert k/v pairs with string values", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_mixed_value_types", - array=[ + arrays=[ {"k": "int", "v": 1}, {"k": "str", "v": "hello"}, {"k": "bool", "v": True}, @@ -49,221 +49,221 @@ expected={"int": 1, "str": "hello", "bool": True, "null": None}, msg="$arrayToObject should convert k/v pairs with mixed value types", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_nested_object_value", - array=[{"k": "obj", "v": {"x": 1, "y": 2}}], + arrays=[{"k": "obj", "v": {"x": 1, "y": 2}}], expected={"obj": {"x": 1, "y": 2}}, msg="$arrayToObject should convert k/v pair with nested object value", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_array_value", - array=[{"k": "arr", "v": [1, 2, 3]}], + arrays=[{"k": "arr", "v": [1, 2, 3]}], expected={"arr": [1, 2, 3]}, msg="$arrayToObject should convert k/v pair with array value", ), ] # Property [Pair Form]: $arrayToObject builds an object from two-element [key, value] arrays. -TWO_ELEM_FORM_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +TWO_ELEM_FORM_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="pair_single", - array=[["a", 1]], + arrays=[["a", 1]], expected={"a": 1}, msg="$arrayToObject should convert single two-element pair", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_multiple", - array=[["a", 1], ["b", 2], ["c", 3]], + arrays=[["a", 1], ["b", 2], ["c", 3]], expected={"a": 1, "b": 2, "c": 3}, msg="$arrayToObject should convert multiple two-element pairs", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_string_values", - array=[["name", "Alice"], ["city", "Mycity"]], + arrays=[["name", "Alice"], ["city", "Mycity"]], expected={"name": "Alice", "city": "Mycity"}, msg="$arrayToObject should convert pairs with string values", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_mixed_value_types", - array=[["int", 1], ["str", "hello"], ["bool", True], ["null", None]], + arrays=[["int", 1], ["str", "hello"], ["bool", True], ["null", None]], expected={"int": 1, "str": "hello", "bool": True, "null": None}, msg="$arrayToObject should convert pairs with mixed value types", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_nested_object_value", - array=[["obj", {"x": 1, "y": 2}]], + arrays=[["obj", {"x": 1, "y": 2}]], expected={"obj": {"x": 1, "y": 2}}, msg="$arrayToObject should convert pair with nested object value", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_array_value", - array=[["arr", [1, 2, 3]]], + arrays=[["arr", [1, 2, 3]]], expected={"arr": [1, 2, 3]}, msg="$arrayToObject should convert pair with array value", ), ] # Property [Empty And Null]: $arrayToObject returns {} for an empty array and null for null input. -EMPTY_AND_NULL_ARRAY_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +EMPTY_AND_NULL_ARRAY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="empty_array", - array=[], + arrays=[], expected={}, msg="$arrayToObject should return empty object for empty array", ), - ArrayToObjectTest( + ArrayTestClass( id="null_array", - array=None, + arrays=None, expected=None, msg="$arrayToObject should return null for null array", ), ] # Property [Duplicate Keys]: when keys repeat, $arrayToObject keeps the last value. -DUPLICATE_KEY_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +DUPLICATE_KEY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="kv_duplicate_keys", - array=[{"k": "a", "v": 1}, {"k": "a", "v": 2}], + arrays=[{"k": "a", "v": 1}, {"k": "a", "v": 2}], expected={"a": 2}, msg="$arrayToObject should keep the last value for duplicate keys (k/v form)", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_duplicate_keys", - array=[["a", 1], ["a", 2]], + arrays=[["a", 1], ["a", 2]], expected={"a": 2}, msg="$arrayToObject should keep the last value for duplicate keys (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_triple_duplicate", - array=[{"k": "x", "v": 1}, {"k": "x", "v": 2}, {"k": "x", "v": 3}], + arrays=[{"k": "x", "v": 1}, {"k": "x", "v": 2}, {"k": "x", "v": 3}], expected={"x": 3}, msg="$arrayToObject should keep the last of three duplicate keys", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_dup_different_types", - array=[["a", 1], ["a", "hello"]], + arrays=[["a", 1], ["a", "hello"]], expected={"a": "hello"}, msg="$arrayToObject should keep the last value even with different value types", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_dup_interspersed", - array=[["a", 1], ["b", 2], ["a", 3]], + arrays=[["a", 1], ["b", 2], ["a", 3]], expected={"a": 3, "b": 2}, msg="$arrayToObject should keep the last value with interspersed duplicate keys", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_dup_interspersed", - array=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "a", "v": 3}], + arrays=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "a", "v": 3}], expected={"a": 3, "b": 2}, msg="$arrayToObject should keep the last value with interspersed duplicates (k/v form)", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_reversed_field_order", - array=[{"v": "val", "k": "key"}], + arrays=[{"v": "val", "k": "key"}], expected={"key": "val"}, msg="$arrayToObject should work regardless of k/v field order in document", ), ] # Property [Key Characters]: $arrayToObject accepts unicode, emoji, and spaced keys. -KEY_EDGE_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +KEY_EDGE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="unicode_key", - array=[{"k": "日本語", "v": 1}], + arrays=[{"k": "日本語", "v": 1}], expected={"日本語": 1}, msg="$arrayToObject should accept a unicode key", ), - ArrayToObjectTest( + ArrayTestClass( id="emoji_key", - array=[{"k": "🔑", "v": "value"}], + arrays=[{"k": "🔑", "v": "value"}], expected={"🔑": "value"}, msg="$arrayToObject should accept an emoji key", ), - ArrayToObjectTest( + ArrayTestClass( id="key_with_spaces", - array=[["key with spaces", 1]], + arrays=[["key with spaces", 1]], expected={"key with spaces": 1}, msg="$arrayToObject should accept a key with spaces", ), - ArrayToObjectTest( + ArrayTestClass( id="numeric_string_keys", - array=[["0", "a"], ["1", "b"]], + arrays=[["0", "a"], ["1", "b"]], expected={"0": "a", "1": "b"}, msg="$arrayToObject should treat numeric string keys as strings", ), - ArrayToObjectTest( + ArrayTestClass( id="underscore_id_key", - array=[["_id", 1]], + arrays=[["_id", 1]], expected={"_id": 1}, msg="$arrayToObject should accept _id as a key", ), - ArrayToObjectTest( + ArrayTestClass( id="operator_like_key", - array=[["$set", 1]], + arrays=[["$set", 1]], expected={"$set": 1}, msg="$arrayToObject should accept an operator-like key", ), - ArrayToObjectTest( + ArrayTestClass( id="very_long_key", - array=[["k" * 1024, 1]], + arrays=[["k" * 1024, 1]], expected={"k" * 1024: 1}, msg="$arrayToObject should not truncate a very long key", ), ] # Property [Field Ordering]: $arrayToObject preserves input order and treats keys case-sensitively. -EDGE_CASE_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +EDGE_CASE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="output_field_order", - array=[["z", 1], ["a", 2], ["m", 3]], + arrays=[["z", 1], ["a", 2], ["m", 3]], expected={"z": 1, "a": 2, "m": 3}, msg="$arrayToObject should preserve input field order in the output", ), - ArrayToObjectTest( + ArrayTestClass( id="case_sensitive_keys_kv", - array=[{"k": "price", "v": 24}, {"k": "PRICE", "v": 100}], + arrays=[{"k": "price", "v": 24}, {"k": "PRICE", "v": 100}], expected={"price": 24, "PRICE": 100}, msg="$arrayToObject should treat case-differing keys as distinct", ), - ArrayToObjectTest( + ArrayTestClass( id="case_sensitive_keys_pair", - array=[["price", 24], ["PRICE", 100]], + arrays=[["price", 24], ["PRICE", 100]], expected={"price": 24, "PRICE": 100}, msg="$arrayToObject should treat case-differing keys as distinct (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="deeply_nested_object_value", - array=[["key", {"a": {"b": {"c": {"d": 1}}}}]], + arrays=[["key", {"a": {"b": {"c": {"d": 1}}}}]], expected={"key": {"a": {"b": {"c": {"d": 1}}}}}, msg="$arrayToObject should handle deeply nested object", ), - ArrayToObjectTest( + ArrayTestClass( id="deeply_nested_array_value", - array=[["key", [[[[1]]]]]], + arrays=[["key", [[[[1]]]]]], expected={"key": [[[[1]]]]}, msg="$arrayToObject should handle deeply nested array", ), - ArrayToObjectTest( + ArrayTestClass( id="empty_object_value", - array=[["key", {}]], + arrays=[["key", {}]], expected={"key": {}}, msg="$arrayToObject should handle empty object value", ), - ArrayToObjectTest( + ArrayTestClass( id="empty_array_value", - array=[["key", []]], + arrays=[["key", []]], expected={"key": []}, msg="$arrayToObject should handle empty array value", ), - ArrayToObjectTest( + ArrayTestClass( id="empty_string_value", - array=[["key", ""]], + arrays=[["key", ""]], expected={"key": ""}, msg="$arrayToObject should handle empty string value", ), - ArrayToObjectTest( + ArrayTestClass( id="large_string_value", - array=[["key", "x" * 10240]], + arrays=[["key", "x" * 10240]], expected={"key": "x" * 10240}, msg="$arrayToObject should handle large string value", ), @@ -291,7 +291,7 @@ @pytest.mark.parametrize("test", pytest_params(TEST_SUBSET_FOR_LITERAL)) def test_arrayToObject_literal(collection, test): """Test $arrayToObject with literal values.""" - result = execute_expression(collection, {"$arrayToObject": {"$literal": test.array}}) + result = execute_expression(collection, {"$arrayToObject": {"$literal": test.arrays}}) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) @@ -301,7 +301,7 @@ def test_arrayToObject_literal(collection, test): def test_arrayToObject_insert(collection, test): """Test $arrayToObject with values from inserted documents.""" result = execute_expression_with_insert( - collection, {"$arrayToObject": "$arr"}, {"arr": test.array} + collection, {"$arrayToObject": "$arr"}, {"arr": test.arrays} ) 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/arrayToObject/test_expression_arrayToObject_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py index 51b982216..3048c00eb 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_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.arrayToObject.utils.arrayToObject_common import ( # noqa: E501 - ArrayToObjectTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -35,332 +35,332 @@ from documentdb_tests.framework.parametrize import pytest_params # Property [Array Type Strictness]: $arrayToObject rejects a non-array input. -NOT_ARRAY_ERROR_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +NOT_ARRAY_ERROR_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="string_input", - array="hello", + arrays="hello", error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject string input", ), - ArrayToObjectTest( + ArrayTestClass( id="int_input", - array=42, + arrays=42, error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject int input", ), - ArrayToObjectTest( + ArrayTestClass( id="bool_input", - array=True, + arrays=True, error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject bool input", ), - ArrayToObjectTest( + ArrayTestClass( id="object_input", - array={"a": 1}, + arrays={"a": 1}, error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject object input", ), - ArrayToObjectTest( + ArrayTestClass( id="double_input", - array=3.14, + arrays=3.14, error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject double input", ), - ArrayToObjectTest( + ArrayTestClass( id="decimal128_input", - array=Decimal128("1"), + arrays=Decimal128("1"), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject decimal128 input", ), - ArrayToObjectTest( + ArrayTestClass( id="int64_input", - array=Int64(1), + arrays=Int64(1), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject int64 input", ), - ArrayToObjectTest( + ArrayTestClass( id="objectid_input", - array=ObjectId(), + arrays=ObjectId(), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject objectid input", ), - ArrayToObjectTest( + ArrayTestClass( id="datetime_input", - array=datetime(2024, 1, 1), + arrays=datetime(2024, 1, 1), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject datetime input", ), - ArrayToObjectTest( + ArrayTestClass( id="binary_input", - array=Binary(b"x", 0), + arrays=Binary(b"x", 0), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject binary input", ), - ArrayToObjectTest( + ArrayTestClass( id="regex_input", - array=Regex("x"), + arrays=Regex("x"), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject regex input", ), - ArrayToObjectTest( + ArrayTestClass( id="maxkey_input", - array=MaxKey(), + arrays=MaxKey(), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject maxkey input", ), - ArrayToObjectTest( + ArrayTestClass( id="minkey_input", - array=MinKey(), + arrays=MinKey(), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject minkey input", ), - ArrayToObjectTest( + ArrayTestClass( id="timestamp_input", - array=Timestamp(0, 0), + arrays=Timestamp(0, 0), error_code=ARRAY_TO_OBJECT_NOT_ARRAY_ERROR, msg="$arrayToObject should reject timestamp input", ), ] # Property [Element Format]: $arrayToObject rejects an element that is not a k/v doc or pair. -INVALID_ELEMENT_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +INVALID_ELEMENT_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="element_is_string", - array=["not_a_pair"], + arrays=["not_a_pair"], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, msg="$arrayToObject should reject string element", ), - ArrayToObjectTest( + ArrayTestClass( id="element_is_int", - array=[42], + arrays=[42], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, msg="$arrayToObject should reject int element", ), - ArrayToObjectTest( + ArrayTestClass( id="element_is_null", - array=[None], + arrays=[None], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, msg="$arrayToObject should reject null element", ), - ArrayToObjectTest( + ArrayTestClass( id="element_is_bool", - array=[True], + arrays=[True], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, msg="$arrayToObject should reject bool element", ), - ArrayToObjectTest( + ArrayTestClass( id="element_is_double", - array=[3.14], + arrays=[3.14], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, msg="$arrayToObject should reject double element", ), - ArrayToObjectTest( + ArrayTestClass( id="element_is_objectid", - array=[ObjectId()], + arrays=[ObjectId()], error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, msg="$arrayToObject should reject ObjectId element", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_missing_v", - array=[{"k": "a"}], + arrays=[{"k": "a"}], error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, msg="$arrayToObject should reject k/v doc missing v field", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_missing_k", - array=[{"v": 1}], + arrays=[{"v": 1}], error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, msg="$arrayToObject should reject k/v doc missing k field", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_extra_field", - array=[{"k": "a", "v": 1, "extra": 2}], + arrays=[{"k": "a", "v": 1, "extra": 2}], error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, msg="$arrayToObject should reject k/v doc with extra field", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_empty_doc", - array=[{}], + arrays=[{}], error_code=ARRAY_TO_OBJECT_INVALID_KV_DOC_ERROR, msg="$arrayToObject should reject empty document", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_wrong_field_names", - array=[{"y": "x", "x": "y"}], + arrays=[{"y": "x", "x": "y"}], error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, msg="$arrayToObject should reject wrong field names", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_uppercase_K", - array=[{"K": "k1", "v": 2}], + arrays=[{"K": "k1", "v": 2}], error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, msg="$arrayToObject should reject uppercase K (case-sensitive)", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_uppercase_V", - array=[{"k": "k1", "V": 2}], + arrays=[{"k": "k1", "V": 2}], error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, msg="$arrayToObject should reject uppercase V (case-sensitive)", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_key_value_names", - array=[{"key": "k1", "value": "v1"}], + arrays=[{"key": "k1", "value": "v1"}], error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, msg="$arrayToObject should reject 'key'/'value' instead of 'k'/'v'", ), - ArrayToObjectTest( + ArrayTestClass( id="mix_valid_pair_and_invalid", - array=[["a", 1], 123], + arrays=[["a", 1], 123], error_code=ARRAY_TO_OBJECT_MIXED_PAIR_THEN_KV_ERROR, msg="$arrayToObject should reject mix of valid pair and invalid element", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_one_element", - array=[["a"]], + arrays=[["a"]], error_code=ARRAY_TO_OBJECT_INVALID_PAIR_ERROR, msg="$arrayToObject should reject one-element array pair", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_three_elements", - array=[["a", 1, 2]], + arrays=[["a", 1, 2]], error_code=ARRAY_TO_OBJECT_INVALID_PAIR_ERROR, msg="$arrayToObject should reject three-element array pair", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_empty_array", - array=[[]], + arrays=[[]], error_code=ARRAY_TO_OBJECT_INVALID_PAIR_ERROR, msg="$arrayToObject should reject empty array pair", ), ] # Property [Key Type Strictness]: $arrayToObject rejects a non-string key. -KEY_NOT_STRING_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +KEY_NOT_STRING_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="kv_int_key", - array=[{"k": 1, "v": "val"}], + arrays=[{"k": 1, "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject int key in k/v form", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_bool_key", - array=[{"k": True, "v": "val"}], + arrays=[{"k": True, "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject bool key in k/v form", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_null_key", - array=[{"k": None, "v": "val"}], + arrays=[{"k": None, "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject null key in k/v form", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_array_key", - array=[{"k": [1], "v": "val"}], + arrays=[{"k": [1], "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject array key in k/v form", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_object_key", - array=[{"k": {"x": 1}, "v": "val"}], + arrays=[{"k": {"x": 1}, "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject object key in k/v form", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_double_key", - array=[{"k": 1.5, "v": "val"}], + arrays=[{"k": 1.5, "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject double key in k/v form", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_int64_key", - array=[{"k": Int64(1), "v": "val"}], + arrays=[{"k": Int64(1), "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject Int64 key in k/v form", ), - ArrayToObjectTest( + ArrayTestClass( id="kv_decimal128_key", - array=[{"k": Decimal128("1"), "v": "val"}], + arrays=[{"k": Decimal128("1"), "v": "val"}], error_code=ARRAY_TO_OBJECT_KV_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject Decimal128 key in k/v form", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_int_key", - array=[[1, "val"]], + arrays=[[1, "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject int key in pair form", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_bool_key", - array=[[True, "val"]], + arrays=[[True, "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject bool key in pair form", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_null_key", - array=[[None, "val"]], + arrays=[[None, "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject null key in pair form", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_array_key", - array=[[[1], "val"]], + arrays=[[[1], "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject array key in pair form", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_object_key", - array=[[{"x": 1}, "val"]], + arrays=[[{"x": 1}, "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject object key in pair form", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_double_key", - array=[[1.5, "val"]], + arrays=[[1.5, "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject double key in pair form", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_int64_key", - array=[[Int64(1), "val"]], + arrays=[[Int64(1), "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject Int64 key in pair form", ), - ArrayToObjectTest( + ArrayTestClass( id="pair_decimal128_key", - array=[[Decimal128("1"), "val"]], + arrays=[[Decimal128("1"), "val"]], error_code=ARRAY_TO_OBJECT_PAIR_KEY_NOT_STRING_ERROR, msg="$arrayToObject should reject Decimal128 key in pair form", ), ] # Property [Mixed Formats]: $arrayToObject rejects arrays mixing k/v doc and pair forms. -MIXED_FORMAT_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +MIXED_FORMAT_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="mixed_kv_then_pair", - array=[{"k": "price", "v": 24}, ["item", "apple"]], + arrays=[{"k": "price", "v": 24}, ["item", "apple"]], error_code=ARRAY_TO_OBJECT_MIXED_KV_THEN_PAIR_ERROR, msg="$arrayToObject should reject a k/v doc followed by a pair", ), - ArrayToObjectTest( + ArrayTestClass( id="mixed_pair_then_kv", - array=[["item", "apple"], {"k": "price", "v": 24}], + arrays=[["item", "apple"], {"k": "price", "v": 24}], error_code=ARRAY_TO_OBJECT_MIXED_PAIR_THEN_KV_ERROR, msg="$arrayToObject should reject a pair followed by a k/v doc", ), ] # Property [Null Byte Key]: $arrayToObject rejects a key containing a null byte. -NULL_BYTE_KEY_TESTS: list[ArrayToObjectTest] = [ - ArrayToObjectTest( +NULL_BYTE_KEY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="null_byte_in_key_pair", - array=[["a\x00b", "value"]], + arrays=[["a\x00b", "value"]], error_code=ARRAY_TO_OBJECT_NULL_BYTE_PAIR_KEY_ERROR, msg="$arrayToObject should reject a null byte in a key (pair form)", ), - ArrayToObjectTest( + ArrayTestClass( id="null_byte_in_key_kv", - array=[{"k": "a\x00b", "v": "value"}], + arrays=[{"k": "a\x00b", "v": "value"}], error_code=ARRAY_TO_OBJECT_NULL_BYTE_KV_KEY_ERROR, msg="$arrayToObject should reject a null byte in a key (k/v form)", ), @@ -379,7 +379,7 @@ def test_arrayToObject_insert(collection, test): """Test $arrayToObject error cases with values from inserted documents.""" result = execute_expression_with_insert( - collection, {"$arrayToObject": "$arr"}, {"arr": test.array} + collection, {"$arrayToObject": "$arr"}, {"arr": test.arrays} ) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg @@ -401,7 +401,7 @@ def test_arrayToObject_insert(collection, test): def test_arrayToObject_literal(collection, test): """Test $arrayToObject error cases with literal values.""" # Use $literal for array inputs to prevent MongoDB from interpreting them as arguments - expr = {"$literal": test.array} if isinstance(test.array, list) else test.array + expr = {"$literal": test.arrays} if isinstance(test.arrays, list) else test.arrays result = execute_expression(collection, {"$arrayToObject": expr}) assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/arrayToObject_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/arrayToObject_common.py deleted file mode 100644 index c51d7eec1..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/utils/arrayToObject_common.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Shared test infrastructure for $arrayToObject expression tests. -""" - -from dataclasses import dataclass -from typing import Any - -from documentdb_tests.framework.test_case import BaseTestCase - - -@dataclass(frozen=True) -class ArrayToObjectTest(BaseTestCase): - """Test case for $arrayToObject operator.""" - - array: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py index 955a6fd08..e3036c052 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_bson_types.py @@ -11,8 +11,8 @@ import pytest from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp -from documentdb_tests.compatibility.tests.core.operator.expressions.array.concatArrays.utils.concatArrays_common import ( # noqa: E501 - ConcatArraysTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -35,20 +35,20 @@ ) # Property [Type Preservation]: $concatArrays preserves each element's BSON type. -BSON_TYPE_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +BSON_TYPE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="int64_values", arrays=[[Int64(1), Int64(2)], [Int64(3)]], expected=[Int64(1), Int64(2), Int64(3)], msg="$concatArrays should preserve Int64 values", ), - ConcatArraysTest( + ArrayTestClass( id="decimal128_values", arrays=[[Decimal128("1.5")], [Decimal128("2.5"), Decimal128("3.5")]], expected=[Decimal128("1.5"), Decimal128("2.5"), Decimal128("3.5")], msg="$concatArrays should preserve Decimal128 values", ), - ConcatArraysTest( + ArrayTestClass( id="datetime_values", arrays=[ [datetime(2024, 1, 1, tzinfo=timezone.utc)], @@ -60,7 +60,7 @@ ], msg="$concatArrays should preserve datetime values", ), - ConcatArraysTest( + ArrayTestClass( id="objectid_values", arrays=[ [ObjectId("000000000000000000000001")], @@ -72,31 +72,31 @@ ], msg="$concatArrays should preserve ObjectId values", ), - ConcatArraysTest( + ArrayTestClass( id="binary_values", arrays=[[Binary(b"\x01", 0)], [Binary(b"\x02", 0)]], expected=[b"\x01", b"\x02"], msg="$concatArrays should preserve Binary values", ), - ConcatArraysTest( + ArrayTestClass( id="regex_values", arrays=[[Regex("^a", "i")], [Regex("^b", "i")]], expected=[Regex("^a", "i"), Regex("^b", "i")], msg="$concatArrays should preserve Regex values", ), - ConcatArraysTest( + ArrayTestClass( id="timestamp_values", arrays=[[Timestamp(1, 0)], [Timestamp(2, 0)]], expected=[Timestamp(1, 0), Timestamp(2, 0)], msg="$concatArrays should preserve Timestamp values", ), - ConcatArraysTest( + ArrayTestClass( id="minkey_maxkey", arrays=[[MinKey()], [MaxKey()]], expected=[MinKey(), MaxKey()], msg="$concatArrays should preserve MinKey/MaxKey values", ), - ConcatArraysTest( + ArrayTestClass( id="uuid_values", arrays=[ [Binary.from_uuid(UUID("01234567-89ab-cdef-fedc-ba9876543210"))], @@ -111,14 +111,14 @@ ] # Property [Mixed Types]: $concatArrays concatenates arrays holding mixed BSON element types. -MIXED_BSON_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +MIXED_BSON_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="mixed_bson_types", arrays=[[1, "two", Int64(3)], [Decimal128("4"), True, None, MinKey()]], expected=[1, "two", Int64(3), Decimal128("4"), True, None, MinKey()], msg="$concatArrays should concatenate mixed BSON types preserving each", ), - ConcatArraysTest( + ArrayTestClass( id="mixed_dates_and_ids", arrays=[ [datetime(2024, 1, 1, tzinfo=timezone.utc), ObjectId("000000000000000000000001")], @@ -132,7 +132,7 @@ ], msg="$concatArrays should concatenate dates, ObjectIds, timestamps, and binary", ), - ConcatArraysTest( + ArrayTestClass( id="mixed_extremes", arrays=[[MinKey(), FLOAT_NEGATIVE_INFINITY, None], [FLOAT_INFINITY, MaxKey()]], expected=[MinKey(), FLOAT_NEGATIVE_INFINITY, None, FLOAT_INFINITY, MaxKey()], @@ -141,26 +141,26 @@ ] # Property [Special Numerics]: $concatArrays preserves NaN, Infinity, and negative zero elements. -SPECIAL_NUMERIC_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +SPECIAL_NUMERIC_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="infinity_values", arrays=[[FLOAT_INFINITY], [FLOAT_NEGATIVE_INFINITY]], expected=[FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY], msg="$concatArrays should preserve infinity values", ), - ConcatArraysTest( + ArrayTestClass( id="decimal128_infinity", arrays=[[DECIMAL128_INFINITY], [DECIMAL128_NEGATIVE_INFINITY]], expected=[DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY], msg="$concatArrays should preserve Decimal128 infinity values", ), - ConcatArraysTest( + ArrayTestClass( id="boundary_values", arrays=[[INT32_MIN, INT32_MAX], [INT64_MIN, INT64_MAX]], expected=[INT32_MIN, INT32_MAX, INT64_MIN, INT64_MAX], msg="$concatArrays should preserve numeric boundary values", ), - ConcatArraysTest( + ArrayTestClass( id="negative_zero", arrays=[[DOUBLE_NEGATIVE_ZERO], [DECIMAL128_NEGATIVE_ZERO]], expected=[DOUBLE_NEGATIVE_ZERO, DECIMAL128_NEGATIVE_ZERO], @@ -169,14 +169,14 @@ ] # Property [Element Identity]: $concatArrays preserves element values and order. -ELEMENT_PRESERVATION_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +ELEMENT_PRESERVATION_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="decimal128_trailing_zeros", arrays=[[Decimal128("1.0")], [Decimal128("1.00"), Decimal128("1.000")]], expected=[Decimal128("1.0"), Decimal128("1.00"), Decimal128("1.000")], msg="$concatArrays should preserve Decimal128 trailing zeros", ), - ConcatArraysTest( + ArrayTestClass( id="decimal128_nan", arrays=[[DECIMAL128_NAN], [Decimal128("1")]], expected=[DECIMAL128_NAN, Decimal128("1")], diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_core_behavior.py index 548d634f8..0080c58d3 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_core_behavior.py @@ -8,8 +8,8 @@ import pytest -from documentdb_tests.compatibility.tests.core.operator.expressions.array.concatArrays.utils.concatArrays_common import ( # noqa: E501 - ConcatArraysTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -19,26 +19,26 @@ from documentdb_tests.framework.parametrize import pytest_params # Property [Concatenation]: $concatArrays joins multiple arrays into one in argument order. -BASIC_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +BASIC_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="two_int_arrays", arrays=[[1, 2], [3, 4]], expected=[1, 2, 3, 4], msg="$concatArrays should concatenate two int arrays", ), - ConcatArraysTest( + ArrayTestClass( id="two_string_arrays", arrays=[["a", "b"], ["c", "d"]], expected=["a", "b", "c", "d"], msg="$concatArrays should concatenate two string arrays", ), - ConcatArraysTest( + ArrayTestClass( id="three_arrays", arrays=[[1, 2], [3, 4], [5, 6]], expected=[1, 2, 3, 4, 5, 6], msg="$concatArrays should concatenate three arrays", ), - ConcatArraysTest( + ArrayTestClass( id="mixed_type_elements", arrays=[[1, "two"], [True, None, {"a": 1}]], expected=[1, "two", True, None, {"a": 1}], @@ -47,44 +47,44 @@ ] # Property [Empty Arrays]: $concatArrays treats empty arrays as contributing no elements. -EMPTY_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +EMPTY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="both_empty", arrays=[[], []], expected=[], msg="$concatArrays should return empty array for two empty arrays", ), - ConcatArraysTest( + ArrayTestClass( id="first_empty", arrays=[[], [1, 2]], expected=[1, 2], msg="$concatArrays should return second array when first is empty", ), - ConcatArraysTest( + ArrayTestClass( id="second_empty", arrays=[[1, 2], []], expected=[1, 2], msg="$concatArrays should return first array when second is empty", ), - ConcatArraysTest( + ArrayTestClass( id="all_empty", arrays=[[], [], []], expected=[], msg="$concatArrays should return empty array for all empty inputs", ), - ConcatArraysTest( + ArrayTestClass( id="no_arguments", arrays=[], expected=[], msg="$concatArrays should return an empty array for no arguments", ), - ConcatArraysTest( + ArrayTestClass( id="empty_between_nonempty", arrays=[[1], [], [2]], expected=[1, 2], msg="$concatArrays should skip an empty array between non-empty arrays", ), - ConcatArraysTest( + ArrayTestClass( id="multiple_empty", arrays=[[], [], [], []], expected=[], @@ -93,14 +93,14 @@ ] # Property [Single Array]: $concatArrays returns a single array argument unchanged. -SINGLE_ARRAY_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +SINGLE_ARRAY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="single_array", arrays=[[1, 2, 3]], expected=[1, 2, 3], msg="$concatArrays should return the single array unchanged", ), - ConcatArraysTest( + ArrayTestClass( id="single_empty_array", arrays=[[]], expected=[], @@ -109,26 +109,26 @@ ] # Property [Top Level Only]: $concatArrays joins at the top level without flattening. -NESTED_ARRAY_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +NESTED_ARRAY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="nested_subarrays", arrays=[[[1, 2]], [[3, 4]]], expected=[[1, 2], [3, 4]], msg="$concatArrays should concatenate top-level, not flatten subarrays", ), - ConcatArraysTest( + ArrayTestClass( id="mixed_nested", arrays=[[[1], "two"], [[3, 4]]], expected=[[1], "two", [3, 4]], msg="$concatArrays should concatenate mixed nested elements", ), - ConcatArraysTest( + ArrayTestClass( id="deeply_nested", arrays=[[[[1]]], [[[2]]]], expected=[[[1]], [[2]]], msg="$concatArrays should preserve deeply nested array elements", ), - ConcatArraysTest( + ArrayTestClass( id="empty_nested", arrays=[[[]], [[]]], expected=[[], []], @@ -137,14 +137,14 @@ ] # Property [Duplicates]: $concatArrays keeps duplicate elements from the inputs. -DUPLICATE_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +DUPLICATE_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="duplicate_elements", arrays=[[1, 2, 3], [2, 3, 4]], expected=[1, 2, 3, 2, 3, 4], msg="$concatArrays should preserve duplicate elements across arrays", ), - ConcatArraysTest( + ArrayTestClass( id="identical_arrays", arrays=[[1, 2], [1, 2]], expected=[1, 2, 1, 2], @@ -153,32 +153,32 @@ ] # Property [Null Propagation]: $concatArrays returns null when any argument is null or missing. -NULL_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +NULL_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="null_first_arg", arrays=[None, [1, 2]], expected=None, msg="$concatArrays should return null when first argument is null", ), - ConcatArraysTest( + ArrayTestClass( id="null_second_arg", arrays=[[1, 2], None], expected=None, msg="$concatArrays should return null when second argument is null", ), - ConcatArraysTest( + ArrayTestClass( id="all_null", arrays=[None, None], expected=None, msg="$concatArrays should return null when all arguments are null", ), - ConcatArraysTest( + ArrayTestClass( id="null_among_three", arrays=[[1], None, [2]], expected=None, msg="$concatArrays should return null when any argument is null", ), - ConcatArraysTest( + ArrayTestClass( id="null_elements_in_arrays", arrays=[[1, None], [None, 2]], expected=[1, None, None, 2], @@ -187,14 +187,14 @@ ] # Property [Object Elements]: $concatArrays concatenates arrays of documents intact. -OBJECT_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +OBJECT_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="arrays_of_objects", arrays=[[{"a": 1}], [{"b": 2}]], expected=[{"a": 1}, {"b": 2}], msg="$concatArrays should concatenate arrays of objects", ), - ConcatArraysTest( + ArrayTestClass( id="objects_with_arrays", arrays=[[{"items": [1, 2]}], [{"items": [3, 4]}]], expected=[{"items": [1, 2]}, {"items": [3, 4]}], @@ -206,26 +206,26 @@ _LARGE_A = list(range(500)) _LARGE_B = list(range(500, 1000)) -LARGE_ARRAY_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +LARGE_ARRAY_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="large_arrays", arrays=[_LARGE_A, _LARGE_B], expected=list(range(1000)), msg="$concatArrays should concatenate large arrays", ), - ConcatArraysTest( + ArrayTestClass( id="two_5000_arrays", arrays=[list(range(5000)), list(range(5000, 10000))], expected=list(range(10000)), msg="$concatArrays should concatenate two large arrays into 10,000 elements", ), - ConcatArraysTest( + ArrayTestClass( id="one_large_one_small", arrays=[list(range(10000)), [10000]], expected=list(range(10001)), msg="$concatArrays should concatenate a large array and a small array", ), - ConcatArraysTest( + ArrayTestClass( id="100_single_element_arrays", arrays=[[i] for i in range(100)], expected=list(range(100)), @@ -234,20 +234,20 @@ ] # Property [Many Arrays]: $concatArrays concatenates many array arguments. -MANY_ARRAYS_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +MANY_ARRAYS_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="five_arrays", arrays=[[1], [2], [3], [4], [5]], expected=[1, 2, 3, 4, 5], msg="$concatArrays should concatenate five arrays", ), - ConcatArraysTest( + ArrayTestClass( id="ten_empty_arrays", arrays=[[] for _ in range(10)], expected=[], msg="$concatArrays should concatenate ten empty arrays", ), - ConcatArraysTest( + ArrayTestClass( id="fifty_arrays", arrays=[[i] for i in range(50)], expected=list(range(50)), diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py index cd1138663..61d9325b8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_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.concatArrays.utils.concatArrays_common import ( # noqa: E501 - ConcatArraysTest, +from documentdb_tests.compatibility.tests.core.operator.expressions.array.utils.array_test_case import ( # noqa: E501 + ArrayTestClass, ) from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -39,110 +39,110 @@ ) # Property [Array Type Strictness]: $concatArrays rejects a non-array argument. -NOT_ARRAY_ERROR_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +NOT_ARRAY_ERROR_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="string_input", arrays=["hello", [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject string input", ), - ConcatArraysTest( + ArrayTestClass( id="int_input", arrays=[42, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject int input", ), - ConcatArraysTest( + ArrayTestClass( id="negative_int_input", arrays=[-42, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject negative int input", ), - ConcatArraysTest( + ArrayTestClass( id="bool_input", arrays=[True, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject bool input", ), - ConcatArraysTest( + ArrayTestClass( id="object_input", arrays=[{"a": 1}, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject object input", ), - ConcatArraysTest( + ArrayTestClass( id="double_input", arrays=[3.14, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject double input", ), - ConcatArraysTest( + ArrayTestClass( id="negative_double_input", arrays=[-3.14, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject negative double input", ), - ConcatArraysTest( + ArrayTestClass( id="decimal128_input", arrays=[Decimal128("1"), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject decimal128 input", ), - ConcatArraysTest( + ArrayTestClass( id="int64_input", arrays=[Int64(1), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject int64 input", ), - ConcatArraysTest( + ArrayTestClass( id="objectid_input", arrays=[ObjectId(), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject objectid input", ), - ConcatArraysTest( + ArrayTestClass( id="datetime_input", arrays=[datetime(2024, 1, 1, tzinfo=timezone.utc), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject datetime input", ), - ConcatArraysTest( + ArrayTestClass( id="binary_input", arrays=[Binary(b"x", 0), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject binary input", ), - ConcatArraysTest( + ArrayTestClass( id="regex_input", arrays=[Regex("x"), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject regex input", ), - ConcatArraysTest( + ArrayTestClass( id="maxkey_input", arrays=[MaxKey(), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject maxkey input", ), - ConcatArraysTest( + ArrayTestClass( id="minkey_input", arrays=[MinKey(), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject minkey input", ), - ConcatArraysTest( + ArrayTestClass( id="timestamp_input", arrays=[Timestamp(0, 0), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject timestamp input", ), - ConcatArraysTest( + ArrayTestClass( id="non_array_second_arg", arrays=[[1], 42], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject non-array in second position", ), - ConcatArraysTest( + ArrayTestClass( id="non_array_middle_arg", arrays=[[1], "bad", [2]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, @@ -151,56 +151,56 @@ ] # Property [Non-Array Numerics]: $concatArrays rejects special float/Decimal128 arguments. -SPECIAL_NUMERIC_ERROR_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +SPECIAL_NUMERIC_ERROR_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="nan_input", arrays=[FLOAT_NAN, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject NaN input", ), - ConcatArraysTest( + ArrayTestClass( id="inf_input", arrays=[FLOAT_INFINITY, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject Infinity input", ), - ConcatArraysTest( + ArrayTestClass( id="neg_inf_input", arrays=[FLOAT_NEGATIVE_INFINITY, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject -Infinity input", ), - ConcatArraysTest( + ArrayTestClass( id="neg_zero_input", arrays=[DOUBLE_NEGATIVE_ZERO, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject negative zero input", ), - ConcatArraysTest( + ArrayTestClass( id="decimal128_nan_input", arrays=[DECIMAL128_NAN, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject Decimal128 NaN input", ), - ConcatArraysTest( + ArrayTestClass( id="decimal128_neg_nan_input", arrays=[Decimal128("-NaN"), [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject Decimal128 -NaN input", ), - ConcatArraysTest( + ArrayTestClass( id="decimal128_inf_input", arrays=[DECIMAL128_INFINITY, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject Decimal128 Infinity input", ), - ConcatArraysTest( + ArrayTestClass( id="decimal128_neg_inf_input", arrays=[DECIMAL128_NEGATIVE_INFINITY, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject Decimal128 -Infinity input", ), - ConcatArraysTest( + ArrayTestClass( id="decimal128_neg_zero_input", arrays=[DECIMAL128_NEGATIVE_ZERO, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, @@ -209,38 +209,38 @@ ] # Property [Non-Array Boundaries]: $concatArrays rejects numeric boundary values as arguments. -BOUNDARY_ERROR_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +BOUNDARY_ERROR_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="int32_max_input", arrays=[INT32_MAX, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject INT32_MAX input", ), - ConcatArraysTest( + ArrayTestClass( id="int32_min_input", arrays=[INT32_MIN, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject INT32_MIN input", ), - ConcatArraysTest( + ArrayTestClass( id="int64_max_input", arrays=[INT64_MAX, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject INT64_MAX input", ), - ConcatArraysTest( + ArrayTestClass( id="int64_min_input", arrays=[INT64_MIN, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject INT64_MIN input", ), - ConcatArraysTest( + ArrayTestClass( id="decimal128_max_input", arrays=[DECIMAL128_MAX, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject DECIMAL128_MAX input", ), - ConcatArraysTest( + ArrayTestClass( id="decimal128_min_input", arrays=[DECIMAL128_MIN, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, @@ -249,20 +249,20 @@ ] # Property [Non-Array Strings]: $concatArrays rejects string arguments regardless of content. -STRING_EDGE_ERROR_TESTS: list[ConcatArraysTest] = [ - ConcatArraysTest( +STRING_EDGE_ERROR_TESTS: list[ArrayTestClass] = [ + ArrayTestClass( id="comma_separated_string_input", arrays=["1, 2, 3", [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject comma-separated string", ), - ConcatArraysTest( + ArrayTestClass( id="json_like_string_input", arrays=["[1, 2, 3]", [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject JSON-like string", ), - ConcatArraysTest( + ArrayTestClass( id="empty_object_input", arrays=[{}, [1]], error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/concatArrays_common.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/concatArrays_common.py deleted file mode 100644 index a2a0d521f..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/utils/concatArrays_common.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Shared test infrastructure for $concatArrays expression tests. -""" - -from dataclasses import dataclass -from typing import Any - -from documentdb_tests.framework.test_case import BaseTestCase - - -@dataclass(frozen=True) -class ConcatArraysTest(BaseTestCase): - """Test case for $concatArrays operator.""" - - arrays: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/__init__.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/utils/__init__.py rename to documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/__init__.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/array_test_case.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/array_test_case.py new file mode 100644 index 000000000..7279ae3c1 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/utils/array_test_case.py @@ -0,0 +1,24 @@ +""" +Shared test case for array expression operator tests. + +Used across the $arrayElemAt, $arrayToObject, and $concatArrays test files. +""" + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class ArrayTestClass(BaseTestCase): + """Test case for array expression operators. + + Attributes: + idx: An index argument (e.g. $arrayElemAt). + arrays: The array input. Holds a single array for $arrayElemAt and + $arrayToObject, or a list of arrays for $concatArrays. + """ + + idx: Any = None + arrays: Any = None From 6e087093c89c096c757e6addf78ace2ef4c91bb8 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 30 Jun 2026 17:02:13 -0700 Subject: [PATCH 4/9] regroup files Signed-off-by: Alina (Xi) Li --- .../test_expression_arrayElemAt_errors.py | 10 +++++ ...test_expression_arrayElemAt_expressions.py | 42 +++++++++++++++---- ...st_expression_arrayToObject_expressions.py | 42 +++++++++++++++++++ .../test_expression_concatArrays_errors.py | 15 +++++++ ...est_expression_concatArrays_expressions.py | 20 ++------- 5 files changed, 103 insertions(+), 26 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py index fa414001d..45da331ef 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py @@ -397,3 +397,13 @@ def test_arrayElemAt_syntax_error(collection, expr): """Test $arrayElemAt errors with wrong number of arguments.""" result = execute_expression(collection, expr) assert_expression_result(result, error_code=EXPRESSION_TYPE_MISMATCH_ERROR) + + +# Property [Index Type Strictness]: $arrayElemAt rejects a field path that resolves to an +# array as the index argument. +def test_arrayElemAt_composite_array_as_index(collection): + """Test $arrayElemAt rejects a composite array from $x.y as the index argument.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": [[10, 20, 30], "$x.y"]}, {"x": [{"y": 0}, {"y": 1}]} + ) + assert_expression_result(result, error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py index 29f8d0100..4c508bb69 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py @@ -14,7 +14,6 @@ execute_expression_with_insert, ) from documentdb_tests.framework.assertions import assertSuccess -from documentdb_tests.framework.error_codes import ARRAY_ELEM_AT_INDEX_TYPE_ERROR # Nested expressions @@ -119,14 +118,6 @@ def test_arrayElemAt_composite_array_as_array(collection): assert_expression_result(result, expected=20) -def test_arrayElemAt_composite_array_as_index(collection): - """Test $arrayElemAt rejects composite array from $x.y as the index argument.""" - result = execute_expression_with_insert( - collection, {"$arrayElemAt": [[10, 20, 30], "$x.y"]}, {"x": [{"y": 0}, {"y": 1}]} - ) - assert_expression_result(result, error_code=ARRAY_ELEM_AT_INDEX_TYPE_ERROR) - - # Composite path with Decimal128 indices and OOB @pytest.mark.parametrize( "idx,expected", @@ -155,3 +146,36 @@ def test_arrayElemAt_composite_path_decimal128_oob(collection, idx): collection, {"$arrayElemAt": ["$a.b", idx]}, {"a": [{"b": 1}, {"b": 2}, {"b": 3}]} ) assertSuccess(result, [{}]) + + +# Property [Array Expression Input]: $arrayElemAt evaluates an array-expression first +# argument whose elements are field references. +@pytest.mark.parametrize( + "idx,expected", + [(0, 10), (1, 20), (-1, 20)], + ids=["array_expr_first", "array_expr_second", "array_expr_negative"], +) +def test_arrayElemAt_array_expression_input(collection, idx, expected): + """Test $arrayElemAt with an array expression of field references as the array argument.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": [["$x", "$y"], idx]}, {"x": 10, "y": 20} + ) + assert_expression_result(result, expected=expected) + + +# Property [Array Index Path]: numeric path components like ".0" address object keys, not +# array positions, in aggregation expression context. +def test_arrayElemAt_object_numeric_key_path(collection): + """Test $arrayElemAt resolves "$a.0.b" through an object key "0", not an array index.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$a.0.b", 1]}, {"a": {"0": {"b": [10, 20, 30]}}} + ) + assert_expression_result(result, expected=20) + + +def test_arrayElemAt_array_index_path_returns_missing(collection): + """Test $arrayElemAt where "$a.0" does not positionally index an array in expression context.""" + result = execute_expression_with_insert( + collection, {"$arrayElemAt": ["$a.0", 0]}, {"a": [[1, 2], [3, 4]]} + ) + assertSuccess(result, [{}]) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py index d6753e282..dad81ddab 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_expressions.py @@ -120,12 +120,54 @@ ), ] +# Property [Expression Inputs]: $arrayToObject evaluates expressions and array-expression +# literals that produce the input array, not just field paths and $literal arrays. +EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="expression_operator_input", + expression={"$arrayToObject": {"$concatArrays": ["$a", "$b"]}}, + doc={"a": [["k1", 1]], "b": [["k2", 2]]}, + expected={"k1": 1, "k2": 2}, + msg="$arrayToObject should evaluate a $concatArrays expression that builds the input array", + ), + ExpressionTestCase( + id="array_expression_input", + expression={"$arrayToObject": [[["$k", "$v"]]]}, + doc={"k": "x", "v": 5}, + expected={"x": 5}, + msg="$arrayToObject should evaluate an array expression containing field references", + ), + ExpressionTestCase( + id="map_expression_input", + expression={ + "$arrayToObject": {"$map": {"input": "$pairs", "as": "p", "in": ["$$p.k", "$$p.v"]}} + }, + doc={"pairs": [{"k": "a", "v": 1}, {"k": "b", "v": 2}]}, + expected={"a": 1, "b": 2}, + msg="$arrayToObject should evaluate a $map expression that builds the input array", + ), +] + +# Property [Array Index Path]: numeric path components like ".0" address object keys, not +# array positions, in aggregation expression context. +ARRAY_INDEX_PATH_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="object_numeric_key_path", + expression={"$arrayToObject": "$a.0"}, + doc={"a": {"0": [["k", 1]]}}, + expected={"k": 1}, + msg="$arrayToObject should resolve a numeric key path through an object, not an index", + ), +] + ALL_EXPR_TESTS = ( FIELD_LOOKUP_TESTS + COMPOSITE_PATH_TESTS + KEY_EDGE_TESTS + SYSTEM_VAR_TESTS + NULL_MISSING_EXPR_TESTS + + EXPRESSION_INPUT_TESTS + + ARRAY_INDEX_PATH_TESTS ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py index 61d9325b8..ccdd29dd2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py @@ -305,3 +305,18 @@ def test_concatArrays_not_array_literal(collection, test): assert_expression_result( result, expected=test.expected, error_code=test.error_code, msg=test.msg ) + + +# Property [Array Type Strictness]: $concatArrays rejects a field path that resolves to a +# non-array value. +def test_concatArrays_field_resolves_to_non_array(collection): + """Test $concatArrays errors when a field path resolves to a non-array value.""" + result = execute_expression_with_insert(collection, {"$concatArrays": ["$a", [1]]}, {"a": 1}) + assert_expression_result(result, error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR) + + +# Property [Array Type Strictness]: $concatArrays rejects an object expression argument. +def test_concatArrays_object_expression_input(collection): + """Test $concatArrays rejects an object expression that is not an array.""" + result = execute_expression_with_insert(collection, {"$concatArrays": [{"a": "$x"}]}, {"x": 1}) + assert_expression_result(result, error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py index 04dd86a55..e7ba3c573 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py @@ -14,7 +14,6 @@ assert_expression_result, execute_expression_with_insert, ) -from documentdb_tests.framework.error_codes import CONCAT_ARRAYS_NOT_ARRAY_ERROR from documentdb_tests.framework.parametrize import pytest_params # Property [Field Path]: $concatArrays resolves field-path array arguments. @@ -146,13 +145,6 @@ expected=None, msg="$concatArrays should return null when an argument is $$REMOVE", ), - ExpressionTestCase( - id="field_ref_wrapped_non_array", - expression={"$concatArrays": ["$a", [1]]}, - doc={"a": 1}, - error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="$concatArrays should error when a field resolves to a non-array", - ), ExpressionTestCase( id="missing_first_field", expression={"$concatArrays": ["$a", "$b"]}, @@ -200,7 +192,7 @@ expression={"$concatArrays": ["$arr", "$null_val", "$int_val"]}, doc={"arr": [1, 2], "null_val": None, "int_val": 42}, expected=None, - msg="$concatArrays should error when a null precedes a non-array argument", + msg="$concatArrays should return null when a null precedes a non-array argument", ), ExpressionTestCase( id="null_result_type_is_null", @@ -276,7 +268,8 @@ ), ] -# Property [Expression Inputs]: $concatArrays evaluates array expressions, rejecting non-arrays. +# Property [Expression Inputs]: $concatArrays evaluates array expressions that produce +# array arguments. EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( id="array_expression_input", @@ -285,13 +278,6 @@ expected=[1, 2, 3], msg="$concatArrays should resolve an array expression containing field references", ), - ExpressionTestCase( - id="object_expression_input", - expression={"$concatArrays": [{"a": "$x"}]}, - doc={"x": 1}, - error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="$concatArrays should reject an object expression that is not an array", - ), ExpressionTestCase( id="literal_then_field", expression={"$concatArrays": [[1, 2, 3], "$a"]}, From 473b6c72cfae85adb57f17681174654f7d41ec77 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 3 Jul 2026 15:29:45 -0700 Subject: [PATCH 5/9] remove duplicate test cases fix flakey test, remove dups, and fix misleading tests Signed-off-by: Alina (Xi) Li --- ...st_expression_arrayElemAt_numeric_index.py | 9 +------ .../test_smoke_expression_arrayElemAt.py | 2 +- ..._expression_arrayToObject_core_behavior.py | 27 ++++++++++++++----- .../test_expression_arrayToObject_errors.py | 4 +-- .../test_smoke_expression_arrayToObject.py | 4 ++- .../test_expression_concatArrays_errors.py | 14 ---------- ...est_expression_concatArrays_expressions.py | 7 ----- .../test_smoke_expression_concatArrays.py | 4 ++- 8 files changed, 30 insertions(+), 41 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py index f8e621099..aa0b427c2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_numeric_index.py @@ -17,7 +17,7 @@ execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import DECIMAL128_NEGATIVE_ZERO, DOUBLE_NEGATIVE_ZERO +from documentdb_tests.framework.test_constants import DECIMAL128_NEGATIVE_ZERO # Property [Numeric Index Types]: $arrayElemAt accepts int32, int64, and integral double indexes. NUMERIC_INDEX_TESTS: list[ArrayTestClass] = [ @@ -77,13 +77,6 @@ expected=10, msg="$arrayElemAt should treat decimal128 -0 as index 0", ), - ArrayTestClass( - id="double_negative_zero_const", - arrays=[10, 20, 30], - idx=DOUBLE_NEGATIVE_ZERO, - expected=10, - msg="$arrayElemAt should treat double -0 const as index 0", - ), ArrayTestClass( id="decimal128_trailing_zero", arrays=[10, 20, 30], diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_smoke_expression_arrayElemAt.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_smoke_expression_arrayElemAt.py index ead213bb8..0699dd680 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_smoke_expression_arrayElemAt.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_smoke_expression_arrayElemAt.py @@ -26,4 +26,4 @@ def test_smoke_expression_arrayElemAt(collection): ) expected = [{"_id": 1, "element": 20}, {"_id": 2, "element": 15}] - assertSuccess(result, expected, "Should support $arrayElemAt expression") + assertSuccess(result, expected, "Should support $arrayElemAt expression", ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py index 527ca5700..b6a3d316d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py @@ -211,14 +211,10 @@ ), ] -# Property [Field Ordering]: $arrayToObject preserves input order and treats keys case-sensitively. +# Property [Key Handling]: $arrayToObject treats keys case-sensitively and preserves arbitrary +# nested/empty values. Output field-order preservation is verified separately in +# test_arrayToObject_preserves_field_order (a plain object comparison is order-insensitive). EDGE_CASE_TESTS: list[ArrayTestClass] = [ - ArrayTestClass( - id="output_field_order", - arrays=[["z", 1], ["a", 2], ["m", 3]], - expected={"z": 1, "a": 2, "m": 3}, - msg="$arrayToObject should preserve input field order in the output", - ), ArrayTestClass( id="case_sensitive_keys_kv", arrays=[{"k": "price", "v": 24}, {"k": "PRICE", "v": 100}], @@ -330,3 +326,20 @@ def test_arrayToObject_large_array_kv(collection): expected=expected, msg="$arrayToObject should build a 10,000-field object from k/v documents", ) + + +def test_arrayToObject_preserves_field_order(collection): + """Test $arrayToObject preserves input pair order in the output object. + + Wrapped in $objectToArray so the assertion observes key order; a plain object + comparison is order-insensitive and would not detect a reordering. + """ + result = execute_expression( + collection, + {"$objectToArray": {"$arrayToObject": {"$literal": [["z", 1], ["a", 2], ["m", 3]]}}}, + ) + assert_expression_result( + result, + expected=[{"k": "z", "v": 1}, {"k": "a", "v": 2}, {"k": "m", "v": 3}], + msg="$arrayToObject should preserve input field order in the output", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py index 3048c00eb..50feed1ca 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py @@ -209,10 +209,10 @@ msg="$arrayToObject should reject 'key'/'value' instead of 'k'/'v'", ), ArrayTestClass( - id="mix_valid_pair_and_invalid", + id="pair_then_non_array_element", arrays=[["a", 1], 123], error_code=ARRAY_TO_OBJECT_MIXED_PAIR_THEN_KV_ERROR, - msg="$arrayToObject should reject mix of valid pair and invalid element", + msg="$arrayToObject should reject a pair followed by a non-array element", ), ArrayTestClass( id="pair_one_element", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_smoke_expression_arrayToObject.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_smoke_expression_arrayToObject.py index 8c49f2e73..8f8dff973 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_smoke_expression_arrayToObject.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_smoke_expression_arrayToObject.py @@ -31,4 +31,6 @@ def test_smoke_expression_arrayToObject(collection): ) expected = [{"_id": 1, "obj": {"a": 1, "b": 2}}, {"_id": 2, "obj": {"x": 10, "y": 20}}] - assertSuccess(result, expected, "Should support $arrayToObject expression") + assertSuccess( + result, expected, "Should support $arrayToObject expression", ignore_doc_order=True + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py index ccdd29dd2..930242160 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_errors.py @@ -182,12 +182,6 @@ error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, msg="$concatArrays should reject Decimal128 NaN input", ), - ArrayTestClass( - id="decimal128_neg_nan_input", - arrays=[Decimal128("-NaN"), [1]], - error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR, - msg="$concatArrays should reject Decimal128 -NaN input", - ), ArrayTestClass( id="decimal128_inf_input", arrays=[DECIMAL128_INFINITY, [1]], @@ -307,14 +301,6 @@ def test_concatArrays_not_array_literal(collection, test): ) -# Property [Array Type Strictness]: $concatArrays rejects a field path that resolves to a -# non-array value. -def test_concatArrays_field_resolves_to_non_array(collection): - """Test $concatArrays errors when a field path resolves to a non-array value.""" - result = execute_expression_with_insert(collection, {"$concatArrays": ["$a", [1]]}, {"a": 1}) - assert_expression_result(result, error_code=CONCAT_ARRAYS_NOT_ARRAY_ERROR) - - # Property [Array Type Strictness]: $concatArrays rejects an object expression argument. def test_concatArrays_object_expression_input(collection): """Test $concatArrays rejects an object expression that is not an array.""" diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py index e7ba3c573..a2ae6ce49 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py @@ -173,13 +173,6 @@ expected=None, msg="$concatArrays should return null when all fields are missing", ), - ExpressionTestCase( - id="explicit_null_field", - expression={"$concatArrays": ["$a", "$b"]}, - doc={"a": None, "b": [1]}, - expected=None, - msg="$concatArrays should return null for an explicit null field", - ), ExpressionTestCase( id="missing_plus_null", expression={"$concatArrays": ["$not_a_field", "$null_val"]}, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_smoke_expression_concatArrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_smoke_expression_concatArrays.py index eae73d8f0..9af309d97 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_smoke_expression_concatArrays.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_smoke_expression_concatArrays.py @@ -28,4 +28,6 @@ def test_smoke_expression_concatArrays(collection): ) expected = [{"_id": 1, "combined": [1, 2, 3, 4]}, {"_id": 2, "combined": [5, 6, 7, 8]}] - assertSuccess(result, expected, "Should support $concatArrays expression") + assertSuccess( + result, expected, "Should support $concatArrays expression", ignore_doc_order=True + ) From 7879a720de6a8cb2f38cc1f4209b74a7e8ea7f98 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 3 Jul 2026 15:43:00 -0700 Subject: [PATCH 6/9] rename tests to be more clearer Signed-off-by: Alina (Xi) Li --- .../test_expression_arrayElemAt_errors.py | 2 +- .../test_expression_arrayToObject_errors.py | 12 ++++++------ .../test_expression_concatArrays_expressions.py | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py index 45da331ef..6c7618a38 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_errors.py @@ -388,7 +388,7 @@ def test_arrayElemAt_insert(collection, test): pytest.param({"$arrayElemAt": [[1, 2, 3]]}, id="one_arg"), pytest.param({"$arrayElemAt": [[1, 2, 3], 0, 1]}, id="three_args"), pytest.param({"$arrayElemAt": []}, id="zero_args"), - pytest.param({"$arrayElemAt": [[[1, 2, 3], 0]]}, id="nested_array_of_args"), + pytest.param({"$arrayElemAt": [[[1, 2, 3], 0]]}, id="nested_pair_not_flattened_to_two_args"), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py index 50feed1ca..0a4042841 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_errors.py @@ -208,12 +208,6 @@ error_code=ARRAY_TO_OBJECT_WRONG_FIELD_NAMES_ERROR, msg="$arrayToObject should reject 'key'/'value' instead of 'k'/'v'", ), - ArrayTestClass( - id="pair_then_non_array_element", - arrays=[["a", 1], 123], - error_code=ARRAY_TO_OBJECT_MIXED_PAIR_THEN_KV_ERROR, - msg="$arrayToObject should reject a pair followed by a non-array element", - ), ArrayTestClass( id="pair_one_element", arrays=[["a"]], @@ -348,6 +342,12 @@ error_code=ARRAY_TO_OBJECT_MIXED_PAIR_THEN_KV_ERROR, msg="$arrayToObject should reject a pair followed by a k/v doc", ), + ArrayTestClass( + id="mixed_pair_then_non_array", + arrays=[["a", 1], 123], + error_code=ARRAY_TO_OBJECT_MIXED_PAIR_THEN_KV_ERROR, + msg="$arrayToObject should reject a pair followed by a non-array element", + ), ] # Property [Null Byte Key]: $arrayToObject rejects a key containing a null byte. diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py index a2ae6ce49..3a7154c51 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/concatArrays/test_expression_concatArrays_expressions.py @@ -40,7 +40,7 @@ msg="$concatArrays should propagate null for a non-existent field", ), ExpressionTestCase( - id="array_index_path", + id="numeric_path_component_not_array_index", expression={"$concatArrays": ["$a.0", [5]]}, doc={"a": [[1, 2], [3, 4]]}, expected=[5], @@ -58,7 +58,8 @@ expression={"$concatArrays": ["$a.b.c", [3]]}, doc={"a": {"b": [{"c": [1]}, {"c": [2]}]}}, expected=[[1], [2], 3], - msg="$concatArrays should resolve a deeply nested field path", + msg="$concatArrays should concatenate an array-of-arrays produced by mapping a field " + "over an array of objects", ), ] From e2570309751d2163d6d0c68ef91aef441cfd126a Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 3 Jul 2026 15:51:40 -0700 Subject: [PATCH 7/9] merge identical functions together Signed-off-by: Alina (Xi) Li --- ...test_expression_arrayElemAt_expressions.py | 130 +++++++----------- ..._expression_arrayToObject_core_behavior.py | 26 ++-- 2 files changed, 57 insertions(+), 99 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py index 4c508bb69..a71d8e788 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayElemAt/test_expression_arrayElemAt_expressions.py @@ -74,108 +74,72 @@ def test_arrayElemAt_nested_expression(collection, expression, expected): assert_expression_result(result, expected=expected) -# Field path lookups +# Field path lookups: $arrayElemAt with array and/or index arguments resolved from inserted +# document fields. These all share one execution path, so they are parametrized into one test. @pytest.mark.parametrize( "document,array_ref,idx,expected", [ ({"a": {"b": [10, 20, 30]}}, "$a.b", 1, 20), ({"a": {"missing": 1}}, "$a.nonexistent", 0, None), ({"a": {"b": {"c": [5, 6, 7]}}}, "$a.b.c", -1, 7), + # field path traverses an array of objects -> collapses to [10, 20] + ({"a": [{"b": 10}, {"b": 20}]}, "$a.b", 0, 10), + # literal array with a field-path index + ({"a": {"b": 1}}, [10, 20, 30], "$a.b", 20), + # array argument resolved from an array of objects + ({"x": [{"y": 10}, {"y": 20}, {"y": 30}]}, "$x.y", 1, 20), + # numeric path component ".0" addresses an object key "0", not an array position + ({"a": {"0": {"b": [10, 20, 30]}}}, "$a.0.b", 1, 20), + # Decimal128 index resolved alongside a composite array path + ({"a": [{"b": 1}, {"b": 2}, {"b": 3}]}, "$a.b", Decimal128("0"), 1), + ({"a": [{"b": 1}, {"b": 2}, {"b": 3}]}, "$a.b", Decimal128("-1"), 3), + # first argument is an array expression whose elements are field references + ({"x": 10, "y": 20}, ["$x", "$y"], 0, 10), + ({"x": 10, "y": 20}, ["$x", "$y"], 1, 20), + ({"x": 10, "y": 20}, ["$x", "$y"], -1, 20), + ], + ids=[ + "nested_field_path", + "nonexistent_field_null", + "deeply_nested_field", + "path_through_array_of_objects", + "composite_path_for_index", + "composite_array_as_array", + "object_numeric_key_path", + "composite_decimal128_pos", + "composite_decimal128_neg", + "array_expr_first", + "array_expr_second", + "array_expr_negative", ], - ids=["nested_field_path", "nonexistent_field_null", "deeply_nested_field"], ) def test_arrayElemAt_field_lookup(collection, document, array_ref, idx, expected): - """Test $arrayElemAt with field path lookups from inserted documents.""" + """Test $arrayElemAt with array/index arguments resolved from inserted document fields.""" result = execute_expression_with_insert( collection, {"$arrayElemAt": [array_ref, idx]}, document ) assert_expression_result(result, expected=expected) -# Field path: path through array of objects -def test_arrayElemAt_path_through_array_of_objects(collection): - """Test $arrayElemAt where field path traverses array of objects.""" - result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$a.b", 0]}, {"a": [{"b": 10}, {"b": 20}]} - ) - assert_expression_result(result, expected=10) - - -# Field path: composite path for index -def test_arrayElemAt_composite_path_for_index(collection): - """Test $arrayElemAt with nested field path as index.""" - result = execute_expression_with_insert( - collection, {"$arrayElemAt": [[10, 20, 30], "$a.b"]}, {"a": {"b": 1}} - ) - assert_expression_result(result, expected=20) - - -def test_arrayElemAt_composite_array_as_array(collection): - """Test $arrayElemAt with composite array from $x.y as the array argument.""" - result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$x.y", 1]}, {"x": [{"y": 10}, {"y": 20}, {"y": 30}]} - ) - assert_expression_result(result, expected=20) - - -# Composite path with Decimal128 indices and OOB +# Field-path lookups that resolve to a missing result: an out-of-bounds Decimal128 index, or a +# numeric path component that does not positionally index an array. Same execution path -> one test. @pytest.mark.parametrize( - "idx,expected", + "document,array_ref,idx", [ - (Decimal128("0"), 1), - (Decimal128("-1"), 3), + ({"a": [{"b": 1}, {"b": 2}, {"b": 3}]}, "$a.b", Decimal128("4")), + ({"a": [{"b": 1}, {"b": 2}, {"b": 3}]}, "$a.b", Decimal128("-4")), + # "$a.0" does not positionally index an array in expression context -> missing + ({"a": [[1, 2], [3, 4]]}, "$a.0", 0), + ], + ids=[ + "composite_decimal128_oob_pos", + "composite_decimal128_oob_neg", + "numeric_path_component_not_array_index", ], - ids=["composite_decimal128_pos", "composite_decimal128_neg"], -) -def test_arrayElemAt_composite_path_decimal128(collection, idx, expected): - """Test $arrayElemAt with composite path $a.b and Decimal128 index.""" - result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$a.b", idx]}, {"a": [{"b": 1}, {"b": 2}, {"b": 3}]} - ) - assert_expression_result(result, expected=expected) - - -@pytest.mark.parametrize( - "idx", - [Decimal128("4"), Decimal128("-4")], - ids=["composite_decimal128_oob_pos", "composite_decimal128_oob_neg"], -) -def test_arrayElemAt_composite_path_decimal128_oob(collection, idx): - """Test $arrayElemAt with composite path $a.b and Decimal128 OOB index.""" - result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$a.b", idx]}, {"a": [{"b": 1}, {"b": 2}, {"b": 3}]} - ) - assertSuccess(result, [{}]) - - -# Property [Array Expression Input]: $arrayElemAt evaluates an array-expression first -# argument whose elements are field references. -@pytest.mark.parametrize( - "idx,expected", - [(0, 10), (1, 20), (-1, 20)], - ids=["array_expr_first", "array_expr_second", "array_expr_negative"], ) -def test_arrayElemAt_array_expression_input(collection, idx, expected): - """Test $arrayElemAt with an array expression of field references as the array argument.""" +def test_arrayElemAt_field_lookup_missing(collection, document, array_ref, idx): + """Test $arrayElemAt returns a missing result for out-of-bounds field-path lookups.""" result = execute_expression_with_insert( - collection, {"$arrayElemAt": [["$x", "$y"], idx]}, {"x": 10, "y": 20} - ) - assert_expression_result(result, expected=expected) - - -# Property [Array Index Path]: numeric path components like ".0" address object keys, not -# array positions, in aggregation expression context. -def test_arrayElemAt_object_numeric_key_path(collection): - """Test $arrayElemAt resolves "$a.0.b" through an object key "0", not an array index.""" - result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$a.0.b", 1]}, {"a": {"0": {"b": [10, 20, 30]}}} - ) - assert_expression_result(result, expected=20) - - -def test_arrayElemAt_array_index_path_returns_missing(collection): - """Test $arrayElemAt where "$a.0" does not positionally index an array in expression context.""" - result = execute_expression_with_insert( - collection, {"$arrayElemAt": ["$a.0", 0]}, {"a": [[1, 2], [3, 4]]} + collection, {"$arrayElemAt": [array_ref, idx]}, document ) assertSuccess(result, [{}]) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py index b6a3d316d..8c418b8f7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/array/arrayToObject/test_expression_arrayToObject_core_behavior.py @@ -304,27 +304,21 @@ def test_arrayToObject_insert(collection, test): ) -def test_arrayToObject_large_array_two_element(collection): - """Test $arrayToObject with 10,000 two-element pairs.""" - large_arr = [[f"key_{i}", i] for i in range(10_000)] - expected = {f"key_{i}": i for i in range(10_000)} - result = execute_expression(collection, {"$arrayToObject": {"$literal": large_arr}}) - assert_expression_result( - result, - expected=expected, - msg="$arrayToObject should build a 10,000-field object from two-element pairs", - ) - - -def test_arrayToObject_large_array_kv(collection): - """Test $arrayToObject with 10,000 k/v documents.""" - large_arr = [{"k": f"key_{i}", "v": i} for i in range(10_000)] +@pytest.mark.parametrize( + "large_arr", + [ + pytest.param([[f"key_{i}", i] for i in range(10_000)], id="two_element_pairs"), + pytest.param([{"k": f"key_{i}", "v": i} for i in range(10_000)], id="kv_documents"), + ], +) +def test_arrayToObject_large_array(collection, large_arr): + """Test $arrayToObject builds a 10,000-field object from pair and k/v forms.""" expected = {f"key_{i}": i for i in range(10_000)} result = execute_expression(collection, {"$arrayToObject": {"$literal": large_arr}}) assert_expression_result( result, expected=expected, - msg="$arrayToObject should build a 10,000-field object from k/v documents", + msg="$arrayToObject should build a 10,000-field object", ) From 7aef89803df6accbc263558f9cbaff6f624d10f1 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 3 Jul 2026 16:28:44 -0700 Subject: [PATCH 8/9] integration tests Signed-off-by: Alina (Xi) Li --- ...t_expressions_combination_arrayToObject.py | 73 ++++ ...array_arrayElemAt_indexOfArray_in_slice.py | 376 ++++++++++++++++++ ...st_expressions_combination_concatArrays.py | 87 ++++ 3 files changed, 536 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_arrayToObject.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_concatArrays.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_arrayToObject.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_arrayToObject.py new file mode 100644 index 000000000..d54502702 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_arrayToObject.py @@ -0,0 +1,73 @@ +""" +Combination tests for $arrayToObject composed with other operators. +""" + +import pytest +from framework.assertions import assertResult +from framework.error_codes import ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR + +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.parametrize import pytest_params + +ARRAY_TO_OBJECT_COMBINATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="arrayToObject_roundtrip_with_objectToArray", + expression={"$arrayToObject": {"$objectToArray": "$obj"}}, + doc={"obj": {"a": 1, "b": 2}}, + expected={"a": 1, "b": 2}, + msg="Roundtrip should restore original object", + ), + ExpressionTestCase( + id="arrayToObject_double_roundtrip", + expression={"$arrayToObject": {"$objectToArray": {"$arrayToObject": "$arr"}}}, + doc={"arr": [["a", 1], ["b", 2]]}, + expected={"a": 1, "b": 2}, + msg="Double roundtrip: array → object → array → object should restore", + ), + ExpressionTestCase( + id="arrayToObject_on_concatArrays", + expression={"$arrayToObject": {"$concatArrays": ["$a", "$b"]}}, + doc={"a": [["a", 1]], "b": [["b", 2]]}, + expected={"a": 1, "b": 2}, + msg="Should work on $concatArrays result", + ), + ExpressionTestCase( + id="arrayToObject_formats_produce_same_result", + expression={ + "$eq": [ + {"$arrayToObject": "$pairs"}, + {"$arrayToObject": "$kv"}, + ] + }, + doc={"pairs": [["a", 1], ["b", 2]], "kv": [{"k": "a", "v": 1}, {"k": "b", "v": 2}]}, + expected=True, + msg="Both formats should produce identical output", + ), + ExpressionTestCase( + id="arrayToObject_on_slice", + expression={"$arrayToObject": {"$slice": ["$arr", 2]}}, + doc={"arr": [["a", 1], ["b", 2], ["c", 3]]}, + expected={"a": 1, "b": 2}, + msg="Should work on $slice result", + ), + ExpressionTestCase( + id="arrayToObject_on_range_fails", + expression={"$arrayToObject": {"$range": ["$start", "$end"]}}, + doc={"start": 0, "end": 3}, + error_code=ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR, + msg="$range produces numbers, should fail", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ARRAY_TO_OBJECT_COMBINATION_TESTS)) +def test_arrayToObject_combination(collection, test): + """Test $arrayToObject composed with other operators.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + expected = [{"result": test.expected}] if test.error_code is None else None + assertResult(result, expected=expected, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py new file mode 100644 index 000000000..ed08f58d9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py @@ -0,0 +1,376 @@ +""" +Combination tests for array expression operators: $arrayElemAt, $indexOfArray, $in, $slice. + +Tests that verify these operators work correctly when composed with each other +and with other operators like $concatArrays, $reverseArray, $filter, $map, $size, etc. +""" + +from dataclasses import dataclass +from typing import Any + +import pytest +from bson import Decimal128, MaxKey, MinKey, Regex +from framework.assertions import assertResult +from framework.test_case import BaseTestCase + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + execute_expression, +) +from documentdb_tests.framework.parametrize import pytest_params + + +@dataclass(frozen=True) +class CombinationTest(BaseTestCase): + """Test case for combination expression tests.""" + + expr: Any = None + + +# --------------------------------------------------------------------------- +# $arrayElemAt combinations +# --------------------------------------------------------------------------- +ARRAY_ELEM_AT_COMBINATION_TESTS: list[CombinationTest] = [ + CombinationTest( + id="arrayElemAt_index_from_indexOfArray", + expr={"$arrayElemAt": [[10, 20, 30], {"$indexOfArray": [[10, 20, 30], 30]}]}, + expected=30, + msg="Should use $indexOfArray result as index", + ), + CombinationTest( + id="arrayElemAt_last_element_via_size", + expr={"$arrayElemAt": [[10, 20, 30], {"$subtract": [{"$size": [[10, 20, 30]]}, 1]}]}, + expected=30, + msg="Should access last element via $size - 1", + ), + CombinationTest( + id="arrayElemAt_elem_from_slice", + expr={"$arrayElemAt": [{"$slice": [[10, 20, 30, 40], -2]}, 0]}, + expected=30, + msg="Should access element from $slice result", + ), + CombinationTest( + id="arrayElemAt_elem_from_slice_3arg", + expr={"$arrayElemAt": [{"$slice": [[10, 20, 30, 40], 1, 2]}, 1]}, + expected=30, + msg="Should access element from $slice 3-arg result", + ), + CombinationTest( + id="arrayElemAt_elem_from_reverseArray", + expr={"$arrayElemAt": [{"$reverseArray": [[10, 20, 30]]}, 0]}, + expected=30, + msg="Should access element from $reverseArray result", + ), + CombinationTest( + id="arrayElemAt_elem_from_concatArrays", + expr={"$arrayElemAt": [{"$concatArrays": [[10, 20], [30, 40]]}, 2]}, + expected=30, + msg="Should access element from $concatArrays result", + ), + CombinationTest( + id="arrayElemAt_computed_index", + expr={"$arrayElemAt": [[10, 20, 30], {"$subtract": [3, 1]}]}, + expected=30, + msg="Should use computed index from $subtract", + ), +] + +# --------------------------------------------------------------------------- +# $in combinations +# --------------------------------------------------------------------------- +IN_COMBINATION_TESTS: list[CombinationTest] = [ + CombinationTest( + id="in_value_from_add", + expr={"$in": [{"$add": [1, 1]}, [1, 2, 3]]}, + expected=True, + msg="Should find value computed by $add", + ), + CombinationTest( + id="in_array_from_concatArrays", + expr={"$in": [3, {"$concatArrays": [[1, 2], [3, 4]]}]}, + expected=True, + msg="Should search in $concatArrays result", + ), + CombinationTest( + id="in_value_from_arrayElemAt", + expr={"$in": [{"$arrayElemAt": [[10, 20, 30], 1]}, [5, 20, 35]]}, + expected=True, + msg="Should find value from $arrayElemAt", + ), + CombinationTest( + id="in_array_from_filter", + expr={ + "$in": [ + 4, + { + "$filter": { + "input": [1, 2, 3, 4, 5], + "as": "n", + "cond": {"$gte": ["$$n", 3]}, + } + }, + ] + }, + expected=True, + msg="Should search in $filter result", + ), + CombinationTest( + id="in_array_from_map", + expr={ + "$in": [ + 20, + { + "$map": { + "input": [1, 2, 3], + "as": "n", + "in": {"$multiply": ["$$n", 10]}, + } + }, + ] + }, + expected=True, + msg="Should search in $map result", + ), + CombinationTest( + id="in_array_from_reverseArray", + expr={"$in": [1, {"$reverseArray": [[1, 2, 3]]}]}, + expected=True, + msg="Should search in $reverseArray result", + ), + CombinationTest( + id="in_cond_with_inner_in", + expr={"$in": [5, {"$cond": [{"$in": ["a", ["a", "b"]]}, [5, 6], [7, 8]]}]}, + expected=True, + msg="Should search in $cond-selected array", + ), + CombinationTest( + id="in_inside_cond", + expr={"$cond": [{"$in": [2, [1, 2, 3]]}, "found", "not_found"]}, + expected="found", + msg="Should use $in result in $cond", + ), + CombinationTest( + id="in_value_from_indexOfArray", + expr={"$in": [{"$indexOfArray": [[10, 20, 30], 20]}, [0, 1, 2]]}, + expected=True, + msg="Should find $indexOfArray result in array", + ), + CombinationTest( + id="in_nested_decimal128", + expr={ + "$in": [ + {"$arrayElemAt": [[Decimal128("1.1"), Decimal128("2.2")], 1]}, + [Decimal128("2.2"), Decimal128("3.3")], + ] + }, + expected=True, + msg="Should find Decimal128 from $arrayElemAt in array", + ), +] + +# --------------------------------------------------------------------------- +# $indexOfArray combinations +# --------------------------------------------------------------------------- +INDEX_OF_ARRAY_COMBINATION_TESTS: list[CombinationTest] = [ + CombinationTest( + id="indexOfArray_result_as_arrayElemAt_index", + expr={"$arrayElemAt": [[10, 20, 30], {"$indexOfArray": [[10, 20, 30], 20]}]}, + expected=20, + msg="Should use $indexOfArray result as $arrayElemAt index", + ), + CombinationTest( + id="indexOfArray_search_from_add", + expr={"$indexOfArray": [[1, 2, 3], {"$add": [1, 1]}]}, + expected=1, + msg="Should search for value computed by $add", + ), + CombinationTest( + id="indexOfArray_array_from_concatArrays", + expr={"$indexOfArray": [{"$concatArrays": [[1, 2], [3, 4]]}, 3]}, + expected=2, + msg="Should search in $concatArrays result", + ), + CombinationTest( + id="indexOfArray_array_from_filter", + expr={ + "$indexOfArray": [ + {"$filter": {"input": [1, 2, 3, 4, 5], "cond": {"$gt": ["$$this", 2]}}}, + 4, + ] + }, + expected=1, + msg="Should search in $filter result", + ), + CombinationTest( + id="indexOfArray_result_in_cond", + expr={"$cond": [{"$gte": [{"$indexOfArray": [[1, 2, 3], 2]}, 0]}, "found", "not_found"]}, + expected="found", + msg="Should use $indexOfArray result in $cond", + ), + CombinationTest( + id="indexOfArray_start_from_subtract", + expr={"$indexOfArray": [[1, 2, 1, 2], 1, {"$subtract": [3, 1]}]}, + expected=2, + msg="Should use $subtract result as start index", + ), + CombinationTest( + id="indexOfArray_via_arrayElemAt", + expr={ + "$indexOfArray": [ + ["a", "b", "c", "d"], + { + "$arrayElemAt": [ + ["a", "b", "c", "d"], + {"$indexOfArray": [[10, 20, 30], 20]}, + ] + }, + ] + }, + expected=1, + msg="Should search for value from nested $arrayElemAt/$indexOfArray", + ), + CombinationTest( + id="indexOfArray_subarray_mixed_bson", + expr={ + "$indexOfArray": [ + [[MinKey(), MaxKey()], [1, 2], "x"], + { + "$arrayElemAt": [ + [[MinKey(), MaxKey()], [1, 2], "x"], + {"$indexOfArray": [[[MinKey(), MaxKey()], [1, 2], "x"], [1, 2]]}, + ] + }, + ] + }, + expected=1, + msg="Should find mixed BSON subarray via nested operators", + ), + CombinationTest( + id="indexOfArray_triple_nested_decimal128", + expr={ + "$indexOfArray": [ + [Decimal128("1.1"), Decimal128("2.2"), Decimal128("3.3")], + { + "$arrayElemAt": [ + [Decimal128("1.1"), Decimal128("2.2"), Decimal128("3.3")], + { + "$indexOfArray": [ + [Decimal128("1.1"), Decimal128("2.2"), Decimal128("3.3")], + Decimal128("3.3"), + ] + }, + ] + }, + ] + }, + expected=2, + msg="Should resolve triple-nested Decimal128 operators", + ), +] + +# --------------------------------------------------------------------------- +# $slice combinations +# --------------------------------------------------------------------------- +SLICE_COMBINATION_TESTS: list[CombinationTest] = [ + CombinationTest( + id="slice_array_from_concatArrays", + expr={"$slice": [{"$concatArrays": [[1, 2], [3, 4, 5]]}, 3]}, + expected=[1, 2, 3], + msg="Should slice $concatArrays result", + ), + CombinationTest( + id="slice_n_from_subtract", + expr={"$slice": [[1, 2, 3, 4, 5], {"$subtract": [5, 2]}]}, + expected=[1, 2, 3], + msg="Should use $subtract result as n", + ), + CombinationTest( + id="slice_array_from_filter", + expr={ + "$slice": [ + { + "$filter": { + "input": [1, 2, 3, 4, 5], + "as": "n", + "cond": {"$gte": ["$$n", 3]}, + } + }, + 2, + ] + }, + expected=[3, 4], + msg="Should slice $filter result", + ), + CombinationTest( + id="slice_position_from_indexOfArray", + expr={ + "$slice": [ + [10, 20, 30, 40, 50], + {"$indexOfArray": [[10, 20, 30, 40, 50], 30]}, + 2, + ] + }, + expected=[30, 40], + msg="Should use $indexOfArray result as position", + ), + CombinationTest( + id="slice_array_from_map", + expr={ + "$slice": [ + { + "$map": { + "input": [1, 2, 3], + "as": "n", + "in": {"$multiply": ["$$n", 10]}, + } + }, + 2, + ] + }, + expected=[10, 20], + msg="Should slice $map result", + ), + CombinationTest( + id="slice_array_from_reverseArray", + expr={"$slice": [{"$reverseArray": [[1, 2, 3, 4, 5]]}, 3]}, + expected=[5, 4, 3], + msg="Should slice $reverseArray result", + ), + CombinationTest( + id="slice_n_from_size", + expr={"$slice": [[10, 20, 30, 40], {"$subtract": [{"$size": [[10, 20, 30, 40]]}, 1]}]}, + expected=[10, 20, 30], + msg="Should use $size-based computation as n", + ), +] + +# --------------------------------------------------------------------------- +# Aggregate all combination tests +# --------------------------------------------------------------------------- +ALL_COMBINATION_TESTS = ( + ARRAY_ELEM_AT_COMBINATION_TESTS + + IN_COMBINATION_TESTS + + INDEX_OF_ARRAY_COMBINATION_TESTS + + SLICE_COMBINATION_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_COMBINATION_TESTS)) +def test_combination_expression(collection, test): + """Test array operators composed with other operators.""" + result = execute_expression(collection, test.expr) + assertResult(result, expected=[{"result": test.expected}], msg=test.msg) + + +# --------------------------------------------------------------------------- +# Standalone tests for behavior that doesn't fit the dataclass pattern +# --------------------------------------------------------------------------- +def test_arrayElemAt_oob_is_missing_not_null(collection): + """Test out-of-bounds result is truly MISSING (field absent), not null.""" + result = execute_expression(collection, {"$type": {"$arrayElemAt": [[1, 2, 3], 10]}}) + assertResult(result, expected=[{"result": "missing"}]) + + +def test_arrayElemAt_regex_type_preserved(collection): + """Test $arrayElemAt preserves regex element type.""" + result = execute_expression(collection, {"$type": {"$arrayElemAt": [[Regex("abc")], 0]}}) + assertResult(result, expected=[{"result": "regex"}]) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_concatArrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_concatArrays.py new file mode 100644 index 000000000..68383890d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_concatArrays.py @@ -0,0 +1,87 @@ +""" +Combination tests for $concatArrays composed with other operators. +""" + +import pytest +from framework.assertions import assertResult +from framework.test_constants import FLOAT_NAN + +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.parametrize import pytest_params + +CONCAT_ARRAYS_COMBINATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + id="concatArrays_on_range", + expression={"$concatArrays": [{"$range": [0, 3]}, {"$range": [3, 6]}]}, + doc={"x": 1}, + expected=[0, 1, 2, 3, 4, 5], + msg="Should concatenate two $range results", + ), + ExpressionTestCase( + id="size_of_concatArrays", + expression={"$size": {"$concatArrays": ["$a", "$b"]}}, + doc={"a": [1, 2], "b": [3, 4, 5]}, + expected=5, + msg="$size on $concatArrays result", + ), + ExpressionTestCase( + id="size_of_empty_concatArrays", + expression={"$size": {"$concatArrays": ["$a", "$b"]}}, + doc={"a": [], "b": []}, + expected=0, + msg="Size of empty concatenation", + ), + ExpressionTestCase( + id="concatArrays_reverseArray_concatArrays", + expression={ + "$concatArrays": [ + {"$reverseArray": {"$concatArrays": ["$a", "$b"]}}, + "$c", + ] + }, + doc={"a": [1, 2], "b": [3], "c": [4]}, + expected=[3, 2, 1, 4], + msg="Should concatenate reversed concat result with another array", + ), + ExpressionTestCase( + id="isArray_on_concatArrays", + expression={"$isArray": {"$concatArrays": ["$a", "$b"]}}, + doc={"a": [1], "b": [2]}, + expected=True, + msg="$isArray on $concatArrays result should return true", + ), + ExpressionTestCase( + id="in_found_in_concatArrays", + expression={"$in": [3, {"$concatArrays": ["$a", "$b"]}]}, + doc={"a": [1, 2], "b": [3, 4]}, + expected=True, + msg="Element found in concatenated array", + ), + ExpressionTestCase( + id="isArray_null_concatArrays", + expression={"$isArray": {"$concatArrays": ["$a", "$b"]}}, + doc={"a": None, "b": [1]}, + expected=False, + msg="Null result is not array", + ), + ExpressionTestCase( + id="float_nan_preserved", + expression={"$arrayElemAt": [{"$concatArrays": ["$a", "$b"]}, 0]}, + doc={"a": [FLOAT_NAN], "b": [1]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="Float NaN element preserved after concatenation", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(CONCAT_ARRAYS_COMBINATION_TESTS)) +def test_concatArrays_combination(collection, test): + """Test $concatArrays composed with other operators.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + expected = [{"result": test.expected}] if test.error_code is None else None + assertResult(result, expected=expected, error_code=test.error_code, msg=test.msg) From 23102095ce25eebd1db9afab0029af9de570813f Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 3 Jul 2026 16:36:34 -0700 Subject: [PATCH 9/9] apply style changes Signed-off-by: Alina (Xi) Li --- .../core/operator/expressions/array/__init__.py | 0 ...test_expressions_combination_arrayToObject.py | 4 ++-- ...on_array_arrayElemAt_indexOfArray_in_slice.py | 16 ++++++++++++---- .../test_expressions_combination_concatArrays.py | 8 +++++--- 4 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/array/__init__.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/array/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/array/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_arrayToObject.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_arrayToObject.py index d54502702..78e4b046f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_arrayToObject.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_arrayToObject.py @@ -3,8 +3,6 @@ """ import pytest -from framework.assertions import assertResult -from framework.error_codes import ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 ExpressionTestCase, @@ -12,6 +10,8 @@ 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.error_codes import ARRAY_TO_OBJECT_INVALID_ELEMENT_ERROR from documentdb_tests.framework.parametrize import pytest_params ARRAY_TO_OBJECT_COMBINATION_TESTS: list[ExpressionTestCase] = [ diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py index ed08f58d9..5916a8e97 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_array_arrayElemAt_indexOfArray_in_slice.py @@ -10,13 +10,13 @@ import pytest from bson import Decimal128, MaxKey, MinKey, Regex -from framework.assertions import assertResult -from framework.test_case import BaseTestCase from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( execute_expression, ) +from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase @dataclass(frozen=True) @@ -367,10 +367,18 @@ def test_combination_expression(collection, test): def test_arrayElemAt_oob_is_missing_not_null(collection): """Test out-of-bounds result is truly MISSING (field absent), not null.""" result = execute_expression(collection, {"$type": {"$arrayElemAt": [[1, 2, 3], 10]}}) - assertResult(result, expected=[{"result": "missing"}]) + assertResult( + result, + expected=[{"result": "missing"}], + msg="Out-of-bounds $arrayElemAt should produce missing field, not null", + ) def test_arrayElemAt_regex_type_preserved(collection): """Test $arrayElemAt preserves regex element type.""" result = execute_expression(collection, {"$type": {"$arrayElemAt": [[Regex("abc")], 0]}}) - assertResult(result, expected=[{"result": "regex"}]) + assertResult( + result, + expected=[{"result": "regex"}], + msg="$arrayElemAt should preserve regex element type", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_concatArrays.py b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_concatArrays.py index 68383890d..6bad4c246 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_concatArrays.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/test_expressions_combination_concatArrays.py @@ -2,9 +2,9 @@ Combination tests for $concatArrays composed with other operators. """ +import math + import pytest -from framework.assertions import assertResult -from framework.test_constants import FLOAT_NAN from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 ExpressionTestCase, @@ -12,7 +12,9 @@ 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 CONCAT_ARRAYS_COMBINATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -73,7 +75,7 @@ id="float_nan_preserved", expression={"$arrayElemAt": [{"$concatArrays": ["$a", "$b"]}, 0]}, doc={"a": [FLOAT_NAN], "b": [1]}, - expected=pytest.approx(FLOAT_NAN, nan_ok=True), + expected=pytest.approx(math.nan, nan_ok=True), msg="Float NaN element preserved after concatenation", ), ]