From a3bf28e0e0f2de205ea5d604aa5eb2dcdbee574c Mon Sep 17 00:00:00 2001 From: "Victor [C] Tsang" Date: Fri, 17 Jul 2026 21:44:34 +0000 Subject: [PATCH 1/2] Add $round, $sigmoid, $sqrt, $trunc tests Co-authored-by: Leszek Kurzyna Signed-off-by: Victor [C] Tsang --- .../expressions/arithmetic/round/__init__.py | 0 .../arithmetic/round/test_round_basic.py | 222 ++++++ .../arithmetic/round/test_round_boundaries.py | 199 +++++ .../round/test_round_bson_type_validation.py | 64 ++ .../arithmetic/round/test_round_errors.py | 335 +++++++++ .../arithmetic/round/test_round_places.py | 364 +++++++++ .../round/test_round_places_precision.py | 187 +++++ .../round/test_round_special_values.py | 210 ++++++ .../arithmetic/sigmoid/__init__.py | 0 .../test_sigmoid_bson_type_validation.py | 62 ++ .../sigmoid/test_sigmoid_composition.py | 71 ++ .../arithmetic/sigmoid/test_sigmoid_errors.py | 99 +++ .../sigmoid/test_sigmoid_numeric.py | 278 +++++++ .../sigmoid/test_sigmoid_numeric_precision.py | 421 +++++++++++ .../expressions/arithmetic/sqrt/__init__.py | 0 .../arithmetic/sqrt/test_sqrt_basic.py | 311 ++++++++ .../sqrt/test_sqrt_bson_type_validation.py | 63 ++ .../arithmetic/sqrt/test_sqrt_composition.py | 93 +++ .../arithmetic/sqrt/test_sqrt_errors.py | 302 ++++++++ .../arithmetic/sqrt/test_sqrt_precision.py | 360 +++++++++ .../expressions/arithmetic/trunc/__init__.py | 0 .../arithmetic/trunc/test_trunc_basic.py | 293 ++++++++ .../arithmetic/trunc/test_trunc_boundaries.py | 314 ++++++++ .../trunc/test_trunc_bson_type_validation.py | 63 ++ .../arithmetic/trunc/test_trunc_errors.py | 297 ++++++++ .../arithmetic/trunc/test_trunc_places.py | 700 ++++++++++++++++++ .../trunc/test_trunc_special_values.py | 342 +++++++++ 27 files changed, 5650 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_basic.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_boundaries.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_bson_type_validation.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_places.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_places_precision.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_special_values.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_bson_type_validation.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_composition.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_numeric.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_numeric_precision.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_basic.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_bson_type_validation.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_composition.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_precision.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_basic.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_boundaries.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_bson_type_validation.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_places.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_special_values.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_basic.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_basic.py new file mode 100644 index 000000000..2c711d54b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_basic.py @@ -0,0 +1,222 @@ +""" +Basic rounding tests for $round expression. + +Covers zero/positive/negative values for all numeric types, rounding +direction (up/down), and round-to-even (banker's rounding) behavior. +""" + +import pytest +from bson import ( + Decimal128, + Int64, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DOUBLE_HALF, + DOUBLE_TWO_AND_HALF, +) + +ROUND_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_zero", + expression={"$round": 0}, + expected=0, + msg="int32 0 rounds to int32 0 (type preserved)", + ), + ExpressionTestCase( + "int64_zero", + expression={"$round": Int64(0)}, + expected=Int64(0), + msg="int64 0 rounds to int64 0 (type preserved)", + ), + ExpressionTestCase( + "double_zero", + expression={"$round": 0.0}, + expected=0.0, + msg="double 0.0 rounds to double 0.0", + ), + ExpressionTestCase( + "double_negative_zero", + expression={"$round": -0.0}, + expected=-0.0, + msg="double -0.0 preserves the negative-zero sign", + ), + ExpressionTestCase( + "decimal_zero", + expression={"$round": Decimal128("0")}, + expected=Decimal128("0"), + msg="decimal 0 rounds to decimal 0 (type preserved)", + ), + ExpressionTestCase( + "decimal_negative_zero", + expression={"$round": Decimal128("-0")}, + expected=Decimal128("-0"), + msg="decimal -0 preserves the negative-zero sign", + ), + ExpressionTestCase( + "int32_positive", + expression={"$round": 1}, + expected=1, + msg="int32 1 is already integral and is returned unchanged", + ), + ExpressionTestCase( + "int64_positive", + expression={"$round": Int64(1)}, + expected=Int64(1), + msg="int64 1 is already integral and is returned unchanged", + ), + ExpressionTestCase( + "int32_negative", + expression={"$round": -1}, + expected=-1, + msg="int32 -1 is already integral and is returned unchanged", + ), + ExpressionTestCase( + "int64_negative", + expression={"$round": Int64(-1)}, + expected=Int64(-1), + msg="int64 -1 is already integral and is returned unchanged", + ), + ExpressionTestCase( + "double_positive", + expression={"$round": 1.5}, + expected=2.0, + msg="double 1.5 rounds half-to-even up to 2.0", + ), + ExpressionTestCase( + "double_negative", + expression={"$round": -1.5}, + expected=-2.0, + msg="double -1.5 rounds half-to-even to -2.0", + ), + ExpressionTestCase( + "double_round_down", + expression={"$round": 1.4}, + expected=1.0, + msg="double 1.4 is below midpoint and rounds down to 1.0", + ), + ExpressionTestCase( + "double_round_up", + expression={"$round": 1.6}, + expected=2.0, + msg="double 1.6 is above midpoint and rounds up to 2.0", + ), + ExpressionTestCase( + "decimal_positive", + expression={"$round": Decimal128("1.5")}, + expected=Decimal128("2"), + msg="decimal 1.5 rounds half-to-even up to 2", + ), + ExpressionTestCase( + "decimal_negative", + expression={"$round": Decimal128("-1.5")}, + expected=Decimal128("-2"), + msg="decimal -1.5 rounds half-to-even to -2", + ), + ExpressionTestCase( + "round_to_even_10_5", + expression={"$round": 10.5}, + expected=10.0, + msg="10.5 rounds half-to-even down to 10.0", + ), + ExpressionTestCase( + "round_to_even_11_5", + expression={"$round": 11.5}, + expected=12.0, + msg="11.5 rounds half-to-even up to 12.0", + ), + ExpressionTestCase( + "round_to_even_12_5", + expression={"$round": 12.5}, + expected=12.0, + msg="12.5 rounds half-to-even down to 12.0", + ), + ExpressionTestCase( + "round_to_even_13_5", + expression={"$round": 13.5}, + expected=14.0, + msg="13.5 rounds half-to-even up to 14.0", + ), + ExpressionTestCase( + "double_half", + expression={"$round": DOUBLE_HALF}, + expected=0.0, + msg="0.5 rounds half-to-even down to 0.0 (0 is even)", + ), + ExpressionTestCase( + "double_two_and_half", + expression={"$round": DOUBLE_TWO_AND_HALF}, + expected=2.0, + msg="2.5 rounds half-to-even down to 2.0 (2 is even)", + ), + ExpressionTestCase( + "decimal_round_to_even_10_5", + expression={"$round": Decimal128("10.5")}, + expected=Decimal128("10"), + msg="decimal 10.5 rounds half-to-even down to 10", + ), + ExpressionTestCase( + "decimal_round_to_even_11_5", + expression={"$round": Decimal128("11.5")}, + expected=Decimal128("12"), + msg="decimal 11.5 rounds half-to-even up to 12", + ), +] + +ROUND_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_positive", + expression={"$round": "$value"}, + doc={"value": 1}, + expected=1, + msg="int32 1 is already integral and is returned unchanged", + ), + ExpressionTestCase( + "int64_negative", + expression={"$round": "$value"}, + doc={"value": Int64(-1)}, + expected=Int64(-1), + msg="int64 -1 is already integral and is returned unchanged", + ), + ExpressionTestCase( + "double_positive", + expression={"$round": "$value"}, + doc={"value": 1.5}, + expected=2.0, + msg="double 1.5 rounds half-to-even up to 2.0", + ), + ExpressionTestCase( + "decimal_positive", + expression={"$round": "$value"}, + doc={"value": Decimal128("1.5")}, + expected=Decimal128("2"), + msg="decimal 1.5 rounds half-to-even up to 2", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ROUND_LITERAL_TESTS)) +def test_round_literal(collection, test): + """Test $round with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ROUND_INSERT_TESTS)) +def test_round_insert(collection, test): + """Test $round with inserted document values""" + 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/arithmetic/round/test_round_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_boundaries.py new file mode 100644 index 000000000..06698c14c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_boundaries.py @@ -0,0 +1,199 @@ +""" +Precision and boundary tests for $round expression. + +Covers IEEE-754 double vs Decimal128 rounding precision, INT32/INT64 +boundary values, double subnormals, and Decimal128 max/min/exponents +and trailing-zero normalization. +""" + +import pytest +from bson import ( + Decimal128, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_LARGE_EXPONENT, + DECIMAL128_MANY_TRAILING_ZEROS, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_SMALL_EXPONENT, + DECIMAL128_TRAILING_ZERO, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + INT32_MAX, + INT32_MIN, + INT64_MAX, + INT64_MIN, +) + +ROUND_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_precision_1_005", + expression={"$round": [Decimal128("1.005"), 2]}, + expected=Decimal128("1.00"), + msg="decimal128 stores 1.005 exactly; half-to-even rounds to 1.00 at place 2", + ), + ExpressionTestCase( + "double_precision_2_015", + expression={"$round": [2.015, 2]}, + expected=2.02, + msg="double 2.015 rounds to 2.02 at place 2", + ), + ExpressionTestCase( + "decimal_precision_2_015", + expression={"$round": [Decimal128("2.015"), 2]}, + expected=Decimal128("2.02"), + msg="decimal128 2.015 rounds to 2.02 at place 2", + ), + ExpressionTestCase( + "int32_max", + expression={"$round": INT32_MAX}, + expected=INT32_MAX, + msg="int32 max is integral and returned unchanged (type preserved)", + ), + ExpressionTestCase( + "int32_min", + expression={"$round": INT32_MIN}, + expected=INT32_MIN, + msg="int32 min is integral and returned unchanged", + ), + ExpressionTestCase( + "int64_max", + expression={"$round": INT64_MAX}, + expected=INT64_MAX, + msg="int64 max is integral and returned unchanged (type preserved)", + ), + ExpressionTestCase( + "int64_min", + expression={"$round": INT64_MIN}, + expected=INT64_MIN, + msg="int64 min is integral and returned unchanged", + ), + ExpressionTestCase( + "double_min_subnormal", + expression={"$round": DOUBLE_MIN_SUBNORMAL}, + expected=0.0, + msg="smallest subnormal double rounds to 0.0", + ), + ExpressionTestCase( + "double_near_min", + expression={"$round": DOUBLE_NEAR_MIN}, + expected=0.0, + msg="tiny normal double (1e-308) rounds to 0.0", + ), + ExpressionTestCase( + "double_near_max", + expression={"$round": DOUBLE_NEAR_MAX}, + expected=DOUBLE_NEAR_MAX, + msg="huge double (1e308) has no fractional part and is unchanged", + ), + ExpressionTestCase( + "double_max_safe_integer", + expression={"$round": float(DOUBLE_MAX_SAFE_INTEGER)}, + expected=float(DOUBLE_MAX_SAFE_INTEGER), + msg="2^53 max-safe-integer double is integral and unchanged", + ), + ExpressionTestCase( + "decimal128_max", + expression={"$round": DECIMAL128_MAX}, + expected=DECIMAL128_MAX, + msg="decimal128 max magnitude has no fractional part and is unchanged", + ), + ExpressionTestCase( + "decimal128_min", + expression={"$round": DECIMAL128_MIN}, + expected=DECIMAL128_MIN, + msg="decimal128 min magnitude has no fractional part and is unchanged", + ), + ExpressionTestCase( + "decimal128_small_exponent", + expression={"$round": DECIMAL128_SMALL_EXPONENT}, + expected=Decimal128("0"), + msg="very small decimal128 (1E-6143) rounds to 0", + ), + ExpressionTestCase( + "decimal128_large_exponent", + expression={"$round": DECIMAL128_LARGE_EXPONENT}, + expected=DECIMAL128_LARGE_EXPONENT, + msg="very large decimal128 (1E+6144) is unchanged", + ), + ExpressionTestCase( + "decimal128_trailing_zero", + expression={"$round": DECIMAL128_TRAILING_ZERO}, + expected=Decimal128("1"), + msg="rounding normalizes decimal 1.0 to 1", + ), + ExpressionTestCase( + "decimal128_many_trailing_zeros", + expression={"$round": DECIMAL128_MANY_TRAILING_ZEROS}, + expected=Decimal128("1"), + msg="rounding normalizes decimal 1.000... to 1", + ), +] + +ROUND_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_precision_2_015", + expression={"$round": ["$value", "$place"]}, + doc={"value": Decimal128("2.015"), "place": 2}, + expected=Decimal128("2.02"), + msg="decimal128 2.015 rounds to 2.02 at place 2", + ), + ExpressionTestCase( + "int32_max", + expression={"$round": "$value"}, + doc={"value": INT32_MAX}, + expected=INT32_MAX, + msg="int32 max is integral and returned unchanged (type preserved)", + ), + ExpressionTestCase( + "int64_min", + expression={"$round": "$value"}, + doc={"value": INT64_MIN}, + expected=INT64_MIN, + msg="int64 min is integral and returned unchanged", + ), + ExpressionTestCase( + "double_min_subnormal", + expression={"$round": "$value"}, + doc={"value": DOUBLE_MIN_SUBNORMAL}, + expected=0.0, + msg="smallest subnormal double rounds to 0.0", + ), + ExpressionTestCase( + "decimal128_large_exponent", + expression={"$round": "$value"}, + doc={"value": DECIMAL128_LARGE_EXPONENT}, + expected=DECIMAL128_LARGE_EXPONENT, + msg="very large decimal128 (1E+6144) is unchanged", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ROUND_LITERAL_TESTS)) +def test_round_literal(collection, test): + """Test $round with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ROUND_INSERT_TESTS)) +def test_round_insert(collection, test): + """Test $round with inserted document values""" + 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/arithmetic/round/test_round_bson_type_validation.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_bson_type_validation.py new file mode 100644 index 000000000..8dbd8aae9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_bson_type_validation.py @@ -0,0 +1,64 @@ +""" +BSON type validation tests for $round expression. + +Systematically verifies that the number (first) input accepts the four numeric +types plus null, and rejects every other BSON type. The `place`-position +validation lives in test_round_errors.py. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertNotError +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import NON_NUMERIC_TYPE_ERROR + +ROUND_BSON_PARAMS = [ + BsonTypeTestCase( + id="number", + msg="$round number should reject non-numeric types", + keyword="number", + # The four numeric types plus null (null propagates to a null result). + valid_types=[ + BsonType.DOUBLE, + BsonType.INT, + BsonType.LONG, + BsonType.DECIMAL, + BsonType.NULL, + ], + default_error_code=NON_NUMERIC_TYPE_ERROR, + ), +] + +REJECTION_CASES = generate_bson_rejection_test_cases(ROUND_BSON_PARAMS) +ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(ROUND_BSON_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", REJECTION_CASES) +def test_round_bson_type_rejected(collection, bson_type, sample_value, spec): + """Verifies $round rejects invalid BSON types for the number input.""" + result = execute_expression_with_insert( + collection, {"$round": "$value"}, {"value": sample_value} + ) + assert_expression_result( + result, + error_code=spec.expected_code(bson_type), + msg=f"{spec.msg}: {bson_type.value}", + ) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) +def test_round_bson_type_accepted(collection, bson_type, sample_value, spec): + """Verifies $round accepts valid numeric BSON types (and null).""" + result = execute_expression_with_insert( + collection, {"$round": "$value"}, {"value": sample_value} + ) + assertNotError(result, msg=f"$round number should accept {bson_type.value}") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_errors.py new file mode 100644 index 000000000..02f01b2c7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_errors.py @@ -0,0 +1,335 @@ +""" +Error tests for $round expression. + +Covers non-numeric type rejection, invalid/out-of-bounds place values, +place type errors (bool, object, array, infinity, fractional decimal128), +arity errors, error-precedence behavior, and composite/array-index field +path rejection. +""" + +import pytest +from bson import ( + Decimal128, + Int64, +) +from bson.binary import Binary + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + EXPRESSION_ARITY_ERROR, + INVALID_DECIMAL_CONVERSION_ERROR, + INVALID_POSITION_ERROR, + INVALID_TYPE_ERROR, + NON_INTEGRAL_POSITION_ERROR, + NON_NUMERIC_TYPE_ERROR, + OUT_OF_RANGE_CONVERSION_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_NAN, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +ROUND_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty_object", + expression={"$round": {}}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Should reject non-numeric object input", + ), + ExpressionTestCase( + "invalid_positive_place_101", + expression={"$round": [123456789012345.123, 101]}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject place value 101 (out of bounds)", + ), + ExpressionTestCase( + "invalid_negative_place_21", + expression={"$round": [123456789012345.123, -21]}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject place value -21 (out of bounds)", + ), + ExpressionTestCase( + "int64_overflow_error", + expression={"$round": [Int64(9223372036854775806), -1]}, + error_code=INVALID_DECIMAL_CONVERSION_ERROR, + msg="Should reject int64 that overflows on rounding", + ), + ExpressionTestCase( + "place_non_integral", + expression={"$round": [1, 10.5]}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject non-integral double place", + ), + ExpressionTestCase( + "place_nan", + expression={"$round": [1, FLOAT_NAN]}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject NaN place", + ), + ExpressionTestCase( + "place_decimal_nan", + expression={"$round": [1, DECIMAL128_NAN]}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject Decimal128 NaN place", + ), + ExpressionTestCase( + "place_string", + expression={"$round": [1, "2"]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject string place", + ), + ExpressionTestCase( + "place_binary", + expression={"$round": [1, Binary(b"", 0)]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject binary place", + ), + ExpressionTestCase( + "place_int64_out_of_bounds", + expression={"$round": [1, Int64(101)]}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject Int64 place out of bounds", + ), + ExpressionTestCase( + "place_decimal_out_of_bounds", + expression={"$round": [1, Decimal128("-21")]}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject Decimal128 place out of bounds", + ), + ExpressionTestCase( + "array_value", + expression={"$round": [1, 2, 3]}, + error_code=EXPRESSION_ARITY_ERROR, + msg="Should reject 3-argument arity", + ), + ExpressionTestCase( + "empty_array", + expression={"$round": []}, + error_code=EXPRESSION_ARITY_ERROR, + msg="Should reject empty array arity", + ), + ExpressionTestCase( + "place_bool", + expression={"$round": [1.5, True]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject boolean place", + ), + ExpressionTestCase( + "place_object", + expression={"$round": [1.5, {"a": 1}]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject object place", + ), + ExpressionTestCase( + "place_array_value", + expression={"$round": [1.5, [2]]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject array place", + ), + ExpressionTestCase( + "place_infinity", + expression={"$round": [1.5, FLOAT_INFINITY]}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject infinity place", + ), + ExpressionTestCase( + "place_negative_infinity", + expression={"$round": [1.5, FLOAT_NEGATIVE_INFINITY]}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject negative infinity place", + ), + ExpressionTestCase( + "place_decimal_fractional", + expression={"$round": [1.5, Decimal128("0.5")]}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject fractional decimal128 place", + ), +] + + +ROUND_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty_object", + expression={"$round": "$value"}, + doc={"value": {}}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Should reject non-numeric object input", + ), + ExpressionTestCase( + "invalid_positive_place_101", + expression={"$round": ["$value", "$place"]}, + doc={"value": 123456789012345.123, "place": 101}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject place value 101 (out of bounds)", + ), + ExpressionTestCase( + "invalid_negative_place_21", + expression={"$round": ["$value", "$place"]}, + doc={"value": 123456789012345.123, "place": -21}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject place value -21 (out of bounds)", + ), + ExpressionTestCase( + "int64_overflow_error", + expression={"$round": ["$value", "$place"]}, + doc={"value": Int64(9223372036854775806), "place": -1}, + error_code=INVALID_DECIMAL_CONVERSION_ERROR, + msg="Should reject int64 that overflows on rounding", + ), + ExpressionTestCase( + "place_non_integral", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1, "place": 10.5}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject non-integral double place", + ), + ExpressionTestCase( + "place_nan", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1, "place": FLOAT_NAN}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject NaN place", + ), + ExpressionTestCase( + "place_decimal_nan", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1, "place": DECIMAL128_NAN}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject Decimal128 NaN place", + ), + ExpressionTestCase( + "place_string", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1, "place": "2"}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject string place", + ), + ExpressionTestCase( + "place_binary", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1, "place": Binary(b"", 0)}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject binary place", + ), + ExpressionTestCase( + "place_int64_out_of_bounds", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1, "place": Int64(101)}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject Int64 place out of bounds", + ), + ExpressionTestCase( + "place_decimal_out_of_bounds", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1, "place": Decimal128("-21")}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject Decimal128 place out of bounds", + ), + ExpressionTestCase( + "empty_array_field", + expression={"$round": "$value"}, + doc={"value": []}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Empty-array field value is non-numeric (not an arity error)", + ), + ExpressionTestCase( + "place_bool", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.5, "place": True}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject boolean place", + ), + ExpressionTestCase( + "place_object", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.5, "place": {"a": 1}}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject object place", + ), + ExpressionTestCase( + "place_array_value", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.5, "place": [2]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject array place", + ), + ExpressionTestCase( + "place_infinity", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.5, "place": FLOAT_INFINITY}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject infinity place", + ), + ExpressionTestCase( + "place_negative_infinity", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.5, "place": FLOAT_NEGATIVE_INFINITY}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject negative infinity place", + ), + ExpressionTestCase( + "place_decimal_fractional", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.5, "place": Decimal128("0.5")}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject fractional decimal128 place", + ), +] + + +ROUND_FIELD_PATH_REJECTION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "composite_array_field", + expression={"$round": "$x.y"}, + doc={"x": [{"y": 1.5}, {"y": 2}]}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Should reject composite array from $x.y on array-of-objects", + ), + ExpressionTestCase( + "array_index_path", + expression={"$round": ["$arr.0", 1]}, + doc={"arr": [1.567, 2.345]}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Should reject $arr.0 on an array (no positional indexing)", + ), + ExpressionTestCase( + "array_index_on_object_key", + expression={"$round": ["$a.0.b", 1]}, + doc={"a": [{"b": 1.567}, {"b": 2.345}]}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Should reject $a.0.b on array-of-objects (no positional indexing)", + ), +] + +ROUND_INSERT_ALL_TESTS: list[ExpressionTestCase] = ( + ROUND_INSERT_TESTS + ROUND_FIELD_PATH_REJECTION_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ROUND_LITERAL_TESTS)) +def test_round_literal(collection, test): + """Test $round with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ROUND_INSERT_ALL_TESTS)) +def test_round_insert(collection, test): + """Test $round with inserted document values, including composite-array + and array-index field path rejection.""" + 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/arithmetic/round/test_round_places.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_places.py new file mode 100644 index 000000000..2f6720f75 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_places.py @@ -0,0 +1,364 @@ +""" +Place-argument and field-lookup tests for $round expression. + +Covers place types (int64, decimal128, whole-double, negative-zero), +int32-to-int64 promotion, null/missing short-circuiting, error-precedence +success cases, midpoint rounding at a place, nested $round, and field-path +resolution. +""" + +import pytest +from bson import ( + Decimal128, + Int64, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + INT32_MAX, + MISSING, +) + +ROUND_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "place_int64", + expression={"$round": [1.12345, Int64(2)]}, + expected=1.12, + msg="int64 place value 2 rounds double 1.12345 to 1.12", + ), + ExpressionTestCase( + "place_decimal", + expression={"$round": [1.12345, Decimal128("2")]}, + expected=1.12, + msg="decimal128 place value 2 rounds double 1.12345 to 1.12", + ), + ExpressionTestCase( + "int32_with_place", + expression={"$round": [5, 3]}, + expected=5, + msg="int32 5 with place 3 is already integral and unchanged", + ), + ExpressionTestCase( + "int64_with_place", + expression={"$round": [Int64(5), 3]}, + expected=Int64(5), + msg="int64 5 with place 3 is already integral and unchanged", + ), + ExpressionTestCase( + "neg_place_int64", + expression={"$round": [Int64(1234), Int64(-2)]}, + expected=Int64(1200), + msg="int64 1234 at place -2 rounds to 1200 (int64 type preserved)", + ), + ExpressionTestCase( + "neg_place_decimal", + expression={"$round": [Decimal128("23.298"), Decimal128("-1")]}, + expected=Decimal128("2E+1"), + msg="decimal128 23.298 at place -1 rounds to 2E+1 (nearest ten)", + ), + ExpressionTestCase( + "int32_to_int64_promotion", + expression={"$round": [INT32_MAX, -1]}, + expected=Int64(2147483650), + msg="rounding INT32_MAX at place -1 overflows int32 and promotes to int64", + ), + ExpressionTestCase( + "place_null", + expression={"$round": [5.7, None]}, + expected=None, + msg="null place short-circuits to a null result", + ), + ExpressionTestCase( + "missing_value", + expression={"$round": MISSING}, + expected=None, + msg="missing input returns null", + ), + ExpressionTestCase( + "place_whole_double", + expression={"$round": [1.2345, 2.0]}, + expected=1.23, + msg="whole-number double 2.0 is accepted as place 2", + ), + ExpressionTestCase( + "place_negative_zero_double", + expression={"$round": [1.2345, -0.0]}, + expected=1.0, + msg="double -0.0 place is treated as place 0", + ), + ExpressionTestCase( + "place_negative_zero_decimal", + expression={"$round": [1.2345, Decimal128("-0")]}, + expected=1.0, + msg="decimal128 -0 place is treated as place 0", + ), + ExpressionTestCase( + "null_num_short_circuit", + expression={"$round": [None, 101]}, + expected=None, + msg="null number short-circuits before place bounds validation", + ), + ExpressionTestCase( + "midpoint_place_2", + expression={"$round": [1.005, 2]}, + expected=1.0, + msg="1.005 is not exactly representable as a double and rounds to 1.0 at place 2", + ), + ExpressionTestCase( + "midpoint_place_1", + expression={"$round": [1.25, 1]}, + expected=1.2, + msg="1.25 rounds half-to-even to 1.2 at place 1", + ), + ExpressionTestCase( + "decimal_midpoint_place_1_65", + expression={"$round": [Decimal128("0.65"), 1]}, + expected=Decimal128("0.6"), + msg="decimal128 0.65 rounds half-to-even down to 0.6 at place 1 (6 is even)", + ), + ExpressionTestCase( + "decimal_midpoint_place_1_75", + expression={"$round": [Decimal128("0.75"), 1]}, + expected=Decimal128("0.8"), + msg="decimal128 0.75 rounds half-to-even up to 0.8 at place 1 (8 is even)", + ), + ExpressionTestCase( + "decimal_midpoint_place_1_85", + expression={"$round": [Decimal128("0.85"), 1]}, + expected=Decimal128("0.8"), + msg="decimal128 0.85 rounds half-to-even down to 0.8 at place 1 (8 is even)", + ), + ExpressionTestCase( + "decimal_midpoint_place_1_95", + expression={"$round": [Decimal128("0.95"), 1]}, + expected=Decimal128("1.0"), + msg="decimal128 0.95 rounds half-to-even up to 1.0 at place 1 (0 is even)", + ), + ExpressionTestCase( + "midpoint_neg_place", + expression={"$round": [15, -1]}, + expected=20, + msg="15 rounds half-to-even up to 20 at place -1", + ), + ExpressionTestCase( + "midpoint_neg_place_25", + expression={"$round": [25, -1]}, + expected=20, + msg="25 rounds half-to-even down to 20 at place -1 (complements 15 rounding up)", + ), + ExpressionTestCase( + "carry_propagation_neg_place", + expression={"$round": [4995, -3]}, + expected=5000, + msg="4995 at place -3 carries across all three digits to 5000", + ), + ExpressionTestCase( + "carry_propagation_neg_place_negative", + expression={"$round": [-4995, -3]}, + expected=-5000, + msg="-4995 at place -3 carries across all three digits to -5000", + ), + ExpressionTestCase( + "carry_propagation_positive_place", + expression={"$round": [3.995, 2]}, + expected=4.0, + msg="3.995 at place 2 carries out of the fractional part into the integer part, 4.0", + ), +] + +ROUND_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "place_int64", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.12345, "place": Int64(2)}, + expected=1.12, + msg="int64 place value 2 rounds double 1.12345 to 1.12", + ), + ExpressionTestCase( + "int32_with_place", + expression={"$round": ["$value", "$place"]}, + doc={"value": 5, "place": 3}, + expected=5, + msg="int32 5 with place 3 is already integral and unchanged", + ), + ExpressionTestCase( + "int64_with_place", + expression={"$round": ["$value", "$place"]}, + doc={"value": Int64(5), "place": 3}, + expected=Int64(5), + msg="int64 5 with place 3 is already integral and unchanged", + ), + ExpressionTestCase( + "neg_place_int64", + expression={"$round": ["$value", "$place"]}, + doc={"value": Int64(1234), "place": Int64(-2)}, + expected=Int64(1200), + msg="int64 1234 at place -2 rounds to 1200 (int64 type preserved)", + ), + ExpressionTestCase( + "neg_place_decimal", + expression={"$round": ["$value", "$place"]}, + doc={"value": Decimal128("23.298"), "place": Decimal128("-1")}, + expected=Decimal128("2E+1"), + msg="decimal128 23.298 at place -1 rounds to 2E+1 (nearest ten)", + ), + ExpressionTestCase( + "int32_to_int64_promotion", + expression={"$round": ["$value", "$place"]}, + doc={"value": INT32_MAX, "place": -1}, + expected=Int64(2147483650), + msg="rounding INT32_MAX at place -1 overflows int32 and promotes to int64", + ), + ExpressionTestCase( + "place_null", + expression={"$round": ["$value", "$place"]}, + doc={"value": 5.7, "place": None}, + expected=None, + msg="null place short-circuits to a null result", + ), + ExpressionTestCase( + "place_whole_double", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.2345, "place": 2.0}, + expected=1.23, + msg="whole-number double 2.0 is accepted as place 2", + ), + ExpressionTestCase( + "place_negative_zero_double", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.2345, "place": -0.0}, + expected=1.0, + msg="double -0.0 place is treated as place 0", + ), + ExpressionTestCase( + "place_negative_zero_decimal", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.2345, "place": Decimal128("-0")}, + expected=1.0, + msg="decimal128 -0 place is treated as place 0", + ), + ExpressionTestCase( + "null_num_short_circuit", + expression={"$round": ["$value", "$place"]}, + doc={"value": None, "place": 101}, + expected=None, + msg="null number short-circuits before place bounds validation", + ), + ExpressionTestCase( + "midpoint_place_2", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.005, "place": 2}, + expected=1.0, + msg="1.005 is not exactly representable as a double and rounds to 1.0 at place 2", + ), + ExpressionTestCase( + "midpoint_place_1", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.25, "place": 1}, + expected=1.2, + msg="1.25 rounds half-to-even to 1.2 at place 1", + ), + ExpressionTestCase( + "decimal_midpoint_place_1_65", + expression={"$round": ["$value", "$place"]}, + doc={"value": Decimal128("0.65"), "place": 1}, + expected=Decimal128("0.6"), + msg="decimal128 0.65 rounds half-to-even down to 0.6 at place 1 (6 is even)", + ), + ExpressionTestCase( + "midpoint_neg_place", + expression={"$round": ["$value", "$place"]}, + doc={"value": 15, "place": -1}, + expected=20, + msg="15 rounds half-to-even up to 20 at place -1", + ), + ExpressionTestCase( + "carry_propagation_neg_place", + expression={"$round": ["$value", "$place"]}, + doc={"value": 4995, "place": -3}, + expected=5000, + msg="4995 at place -3 carries across all three digits to 5000", + ), + ExpressionTestCase( + "carry_propagation_neg_place_negative", + expression={"$round": ["$value", "$place"]}, + doc={"value": -4995, "place": -3}, + expected=-5000, + msg="-4995 at place -3 carries across all three digits to -5000", + ), + ExpressionTestCase( + "carry_propagation_positive_place", + expression={"$round": ["$value", "$place"]}, + doc={"value": 3.995, "place": 2}, + expected=4.0, + msg="3.995 at place 2 carries out of the fractional part into the integer part, 4.0", + ), +] + +ROUND_NESTED_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_round_2", + expression={"$round": {"$round": 4.6}}, + expected=5.0, + msg="Should evaluate nested $round", + ), + ExpressionTestCase( + "nested_round_2_midpoint", + expression={"$round": {"$round": 1.55}}, + expected=2.0, + msg="Should evaluate nested $round at midpoint", + ), + ExpressionTestCase( + "nested_round_3_with_place", + expression={"$round": [{"$round": [{"$round": [3.456, 2]}, 1]}]}, + expected=4.0, + msg="Should evaluate triple-nested $round with place", + ), +] + +ROUND_FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_field", + expression={"$round": "$a.b"}, + doc={"a": {"b": 4.6}}, + expected=5.0, + msg="Should resolve nested field path", + ), + ExpressionTestCase( + "nonexistent_field", + expression={"$round": "$a.nonexistent"}, + doc={"a": {"missing": 1}}, + expected=None, + msg="Should return null for nonexistent field path", + ), +] + + +ROUND_LITERAL_ALL_TESTS: list[ExpressionTestCase] = ROUND_LITERAL_TESTS + ROUND_NESTED_TESTS +ROUND_INSERT_ALL_TESTS: list[ExpressionTestCase] = ROUND_INSERT_TESTS + ROUND_FIELD_LOOKUP_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ROUND_LITERAL_ALL_TESTS)) +def test_round_literal(collection, test): + """Test $round with literal values and nested expressions.""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ROUND_INSERT_ALL_TESTS)) +def test_round_insert(collection, test): + """Test $round with inserted document values and field-path lookups.""" + 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/arithmetic/round/test_round_places_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_places_precision.py new file mode 100644 index 000000000..0cb799c1d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_places_precision.py @@ -0,0 +1,187 @@ +""" +High-precision and place-range tests for $round expression. + +Covers decimal128 34-digit place rounding, negative-place range sweeps on +large doubles, and double precision-limit places. +""" + +import pytest +from bson import ( + Decimal128, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +ROUND_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "place_32", + expression={"$round": [Decimal128("1.123456789012345678901234567890123"), 32]}, + expected=Decimal128("1.12345678901234567890123456789012"), + msg="decimal128 rounds to 32 places (drops the last of 33 fractional digits)", + ), + ExpressionTestCase( + "place_33", + expression={"$round": [Decimal128("1.123456789012345678901234567890123"), 33]}, + expected=Decimal128("1.123456789012345678901234567890123"), + msg="decimal128 rounds to 33 places (all fractional digits retained)", + ), + ExpressionTestCase( + "place_34", + expression={"$round": [Decimal128("1.123456789012345678901234567890123"), 34]}, + expected=Decimal128("1.1234567890123456789012345678901230"), + msg="decimal128 rounds to 34 places (pads one trailing zero)", + ), + ExpressionTestCase( + "place_99", + expression={"$round": [Decimal128("1.123456789012345678901234567890123"), 99]}, + expected=Decimal128("1.1234567890123456789012345678901230"), + msg="decimal128 place 99 exceeds precision; value retained", + ), + ExpressionTestCase( + "place_100", + expression={"$round": [Decimal128("1.123456789012345678901234567890123"), 100]}, + expected=Decimal128("1.1234567890123456789012345678901230"), + msg="decimal128 place 100 (max) exceeds precision; value retained", + ), + ExpressionTestCase( + "neg_place_-1", + expression={"$round": [123456789012345.12, -1]}, + expected=pytest.approx(123456789012350.0), + msg="large double at place -1 rounds to the nearest ten", + ), + ExpressionTestCase( + "neg_place_-2", + expression={"$round": [123456789012345.12, -2]}, + expected=123456789012300.0, + msg="large double at place -2 rounds to the nearest hundred", + ), + ExpressionTestCase( + "neg_place_-12", + expression={"$round": [123456789012345.12, -12]}, + expected=123000000000000.0, + msg="large double at place -12 rounds to the nearest 1e12", + ), + ExpressionTestCase( + "neg_place_-14", + expression={"$round": [123456789012345.12, -14]}, + expected=100000000000000.0, + msg="large double at place -14 rounds to the nearest 1e14", + ), + ExpressionTestCase( + "neg_place_-15", + expression={"$round": [123456789012345.12, -15]}, + expected=0.0, + msg="large double at place -15 rounds down to 0.0", + ), + ExpressionTestCase( + "neg_place_-19", + expression={"$round": [123456789012345.12, -19]}, + expected=0.0, + msg="large double at place -19 rounds down to 0.0", + ), + ExpressionTestCase( + "neg_place_-20", + expression={"$round": [123456789012345.12, -20]}, + expected=0.0, + msg="large double at place -20 (min) rounds down to 0.0", + ), + ExpressionTestCase( + "double_place_0", + expression={"$round": [1.12345678901234, 0]}, + expected=1.0, + msg="double 1.12345678901234 rounds to 1.0 at place 0", + ), + ExpressionTestCase( + "double_place_2", + expression={"$round": [1.12345678901234, 2]}, + expected=1.12, + msg="double 1.12345678901234 rounds to 1.12 at place 2", + ), + ExpressionTestCase( + "double_place_13", + expression={"$round": [1.12345678901234, 13]}, + expected=1.1234567890123, + msg="double rounds to 13 places (near double precision limit)", + ), + ExpressionTestCase( + "double_place_14", + expression={"$round": [1.12345678901234, 14]}, + expected=1.12345678901234, + msg="double rounds to 14 places (full input precision, unchanged)", + ), + ExpressionTestCase( + "double_place_20", + expression={"$round": [1.12345678901234, 20]}, + expected=1.12345678901234, + msg="place 20 exceeds double precision; value returned unchanged", + ), + ExpressionTestCase( + "decimal_place_0", + expression={"$round": [Decimal128("1.123456789012345678901234567890123"), 0]}, + expected=Decimal128("1"), + msg="34-digit decimal rounds to 1 at place 0", + ), + ExpressionTestCase( + "decimal_place_2", + expression={"$round": [Decimal128("1.123456789012345678901234567890123"), 2]}, + expected=Decimal128("1.12"), + msg="34-digit decimal rounds to 1.12 at place 2", + ), +] + +ROUND_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "place_32", + expression={"$round": ["$value", "$place"]}, + doc={"value": Decimal128("1.123456789012345678901234567890123"), "place": 32}, + expected=Decimal128("1.12345678901234567890123456789012"), + msg="decimal128 rounds to 32 places (drops the last of 33 fractional digits)", + ), + ExpressionTestCase( + "place_100", + expression={"$round": ["$value", "$place"]}, + doc={"value": Decimal128("1.123456789012345678901234567890123"), "place": 100}, + expected=Decimal128("1.1234567890123456789012345678901230"), + msg="decimal128 place 100 (max) exceeds precision; value retained", + ), + ExpressionTestCase( + "neg_place_-20", + expression={"$round": ["$value", "$place"]}, + doc={"value": 123456789012345.12, "place": -20}, + expected=0.0, + msg="large double at place -20 (min) rounds down to 0.0", + ), + ExpressionTestCase( + "double_place_14", + expression={"$round": ["$value", "$place"]}, + doc={"value": 1.12345678901234, "place": 14}, + expected=1.12345678901234, + msg="double rounds to 14 places (full input precision, unchanged)", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ROUND_LITERAL_TESTS)) +def test_round_literal(collection, test): + """Test $round with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ROUND_INSERT_TESTS)) +def test_round_insert(collection, test): + """Test $round with inserted document values""" + 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/arithmetic/round/test_round_special_values.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_special_values.py new file mode 100644 index 000000000..fd66e853b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_special_values.py @@ -0,0 +1,210 @@ +""" +Half-value and special-value tests for $round expression. + +Covers half-boundary precision for double and Decimal128 (including +just-below/just-above-half and negative-zero results), positive/negative +Infinity, NaN, and null. +""" + +import math + +import pytest +from bson import ( + Decimal128, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_HALF, + DECIMAL128_INFINITY, + DECIMAL128_JUST_ABOVE_HALF, + DECIMAL128_JUST_BELOW_HALF, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_HALF, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ONE_AND_HALF, + DECIMAL128_ONE_AND_HALF, + DECIMAL128_TWO_AND_HALF, + DOUBLE_JUST_ABOVE_HALF, + DOUBLE_JUST_BELOW_HALF, + DOUBLE_NEGATIVE_HALF, + DOUBLE_NEGATIVE_ONE_AND_HALF, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +ROUND_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_negative_half", + expression={"$round": DOUBLE_NEGATIVE_HALF}, + expected=-0.0, + msg="-0.5 rounds half-to-even to -0.0 (preserves negative-zero sign)", + ), + ExpressionTestCase( + "double_negative_one_and_half", + expression={"$round": DOUBLE_NEGATIVE_ONE_AND_HALF}, + expected=-2.0, + msg="-1.5 rounds half-to-even to -2.0", + ), + ExpressionTestCase( + "double_just_below_half", + expression={"$round": DOUBLE_JUST_BELOW_HALF}, + expected=0.0, + msg="0.4999999999999994 is below midpoint and rounds down to 0.0", + ), + ExpressionTestCase( + "double_just_above_half", + expression={"$round": DOUBLE_JUST_ABOVE_HALF}, + expected=1.0, + msg="0.500000001 is above midpoint and rounds up to 1.0", + ), + ExpressionTestCase( + "decimal_half", + expression={"$round": DECIMAL128_HALF}, + expected=Decimal128("0"), + msg="decimal 0.5 rounds half-to-even down to 0", + ), + ExpressionTestCase( + "decimal_one_and_half", + expression={"$round": DECIMAL128_ONE_AND_HALF}, + expected=Decimal128("2"), + msg="decimal 1.5 rounds half-to-even up to 2", + ), + ExpressionTestCase( + "decimal_two_and_half", + expression={"$round": DECIMAL128_TWO_AND_HALF}, + expected=Decimal128("2"), + msg="decimal 2.5 rounds half-to-even down to 2", + ), + ExpressionTestCase( + "decimal_negative_half", + expression={"$round": DECIMAL128_NEGATIVE_HALF}, + expected=Decimal128("-0"), + msg="decimal -0.5 rounds half-to-even to -0 (preserves negative-zero sign)", + ), + ExpressionTestCase( + "decimal_negative_one_and_half", + expression={"$round": DECIMAL128_NEGATIVE_ONE_AND_HALF}, + expected=Decimal128("-2"), + msg="decimal -1.5 rounds half-to-even to -2", + ), + ExpressionTestCase( + "decimal_just_below_half", + expression={"$round": DECIMAL128_JUST_BELOW_HALF}, + expected=Decimal128("0"), + msg="34-digit decimal just below 0.5 rounds down to 0", + ), + ExpressionTestCase( + "decimal_just_above_half", + expression={"$round": DECIMAL128_JUST_ABOVE_HALF}, + expected=Decimal128("1"), + msg="34-digit decimal just above 0.5 rounds up to 1", + ), + ExpressionTestCase( + "float_infinity", + expression={"$round": FLOAT_INFINITY}, + expected=FLOAT_INFINITY, + msg="double +Infinity is returned unchanged", + ), + ExpressionTestCase( + "float_negative_infinity", + expression={"$round": FLOAT_NEGATIVE_INFINITY}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="double -Infinity is returned unchanged", + ), + ExpressionTestCase( + "decimal128_infinity", + expression={"$round": DECIMAL128_INFINITY}, + expected=DECIMAL128_INFINITY, + msg="decimal128 +Infinity is returned unchanged", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + expression={"$round": DECIMAL128_NEGATIVE_INFINITY}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="decimal128 -Infinity is returned unchanged", + ), + ExpressionTestCase( + "float_nan", + expression={"$round": FLOAT_NAN}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="double NaN rounds to NaN", + ), + ExpressionTestCase( + "decimal128_nan", + expression={"$round": DECIMAL128_NAN}, + expected=DECIMAL128_NAN, + msg="decimal128 NaN rounds to NaN", + ), + ExpressionTestCase( + "null_value", + expression={"$round": None}, + expected=None, + msg="null input returns null", + ), +] + +ROUND_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_negative_half", + expression={"$round": "$value"}, + doc={"value": DOUBLE_NEGATIVE_HALF}, + expected=-0.0, + msg="-0.5 rounds half-to-even to -0.0 (preserves negative-zero sign)", + ), + ExpressionTestCase( + "decimal_one_and_half", + expression={"$round": "$value"}, + doc={"value": DECIMAL128_ONE_AND_HALF}, + expected=Decimal128("2"), + msg="decimal 1.5 rounds half-to-even up to 2", + ), + ExpressionTestCase( + "float_infinity", + expression={"$round": "$value"}, + doc={"value": FLOAT_INFINITY}, + expected=FLOAT_INFINITY, + msg="double +Infinity is returned unchanged", + ), + ExpressionTestCase( + "float_nan", + expression={"$round": "$value"}, + doc={"value": FLOAT_NAN}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="double NaN rounds to NaN", + ), + ExpressionTestCase( + "null_value", + expression={"$round": "$value"}, + doc={"value": None}, + expected=None, + msg="null input returns null", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ROUND_LITERAL_TESTS)) +def test_round_literal(collection, test): + """Test $round with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(ROUND_INSERT_TESTS)) +def test_round_insert(collection, test): + """Test $round with inserted document values""" + 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/arithmetic/sigmoid/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_bson_type_validation.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_bson_type_validation.py new file mode 100644 index 000000000..c4e2cc94a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_bson_type_validation.py @@ -0,0 +1,62 @@ +""" +BSON type validation tests for $sigmoid expression. + +Systematically verifies that the single numeric input accepts the four numeric +types plus null, and rejects every other BSON type (generic code 14). +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertNotError +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import TYPE_MISMATCH_ERROR + +SIGMOID_BSON_PARAMS = [ + BsonTypeTestCase( + id="input", + msg="$sigmoid should reject non-numeric input", + keyword="input", + valid_types=[ + BsonType.DOUBLE, + BsonType.INT, + BsonType.LONG, + BsonType.DECIMAL, + BsonType.NULL, + ], + default_error_code=TYPE_MISMATCH_ERROR, + ), +] + +REJECTION_CASES = generate_bson_rejection_test_cases(SIGMOID_BSON_PARAMS) +ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(SIGMOID_BSON_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", REJECTION_CASES) +def test_sigmoid_bson_type_rejected(collection, bson_type, sample_value, spec): + """Verifies $sigmoid rejects invalid BSON types.""" + result = execute_expression_with_insert( + collection, {"$sigmoid": "$value"}, {"value": sample_value} + ) + assert_expression_result( + result, + error_code=spec.expected_code(bson_type), + msg=f"{spec.msg}: {bson_type.value}", + ) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) +def test_sigmoid_bson_type_accepted(collection, bson_type, sample_value, spec): + """Verifies $sigmoid accepts valid numeric BSON types (and null).""" + result = execute_expression_with_insert( + collection, {"$sigmoid": "$value"}, {"value": sample_value} + ) + assertNotError(result, msg=f"$sigmoid should accept {bson_type.value}") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_composition.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_composition.py new file mode 100644 index 000000000..286794751 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_composition.py @@ -0,0 +1,71 @@ +""" +arithmetic $sigmoid tests. + +Nested-expression composition and field-path lookup tests for $sigmoid. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +SIGMOID_NESTED_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_sigmoid_2", + expression={"$sigmoid": {"$sigmoid": 0}}, + expected=pytest.approx(0.6224593312018546), + msg="Should evaluate nested $sigmoid", + ), + ExpressionTestCase( + "nested_sigmoid_3", + expression={"$sigmoid": {"$sigmoid": {"$sigmoid": 0}}}, + expected=pytest.approx(0.6507776782147005), + msg="Should evaluate triple-nested $sigmoid", + ), +] + + +SIGMOID_FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_field", + expression={"$sigmoid": "$a.b"}, + doc={"a": {"b": 1}}, + expected=pytest.approx(0.7310585786300049), + msg="Should resolve nested field path", + ), + ExpressionTestCase( + "nonexistent_field", + expression={"$sigmoid": "$a.nonexistent"}, + doc={"a": {"missing": 1}}, + expected=None, + msg="Should return null for nonexistent field path", + ), + ExpressionTestCase( + "deeply_nested_field", + expression={"$sigmoid": "$a.b.c.d"}, + doc={"a": {"b": {"c": {"d": -1}}}}, + expected=pytest.approx(0.2689414213699951), + msg="Should resolve deeply nested field path", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SIGMOID_NESTED_TESTS)) +def test_sigmoid_nested_expression(collection, test): + """Test $sigmoid can take an expression as input""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +@pytest.mark.parametrize("test", pytest_params(SIGMOID_FIELD_LOOKUP_TESTS)) +def test_sigmoid_field_lookup(collection, test): + """Test $sigmoid with field path lookups""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result(result, expected=test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_errors.py new file mode 100644 index 000000000..a06bf76d7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_errors.py @@ -0,0 +1,99 @@ +""" +arithmetic $sigmoid tests. + +Error-case tests for the arithmetic $sigmoid operator: non-numeric literal +and inserted values (empty object/array, multi-element array) and composite +array field-path rejection. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import TYPE_MISMATCH_ERROR +from documentdb_tests.framework.parametrize import pytest_params + +# Non-numeric and arity-invalid inputs surface the generic TYPE_MISMATCH_ERROR +# (14) rather than the dedicated type/arity codes used by $sqrt/$round. +SIGMOID_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty_object", + expression={"$sigmoid": {}}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject empty object in literal", + ), + ExpressionTestCase( + "empty_array", + expression={"$sigmoid": []}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject empty array in literal", + ), + ExpressionTestCase( + "single_element_array", + expression={"$sigmoid": [1]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject single-element array (not treated as unary form)", + ), + ExpressionTestCase( + "array_value", + expression={"$sigmoid": [1, 2]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject multi-element array value in literal", + ), +] + + +SIGMOID_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty_object", + expression={"$sigmoid": "$value"}, + doc={"value": {}}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject empty object in insert", + ), + ExpressionTestCase( + "empty_array", + expression={"$sigmoid": "$value"}, + doc={"value": []}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject empty array in insert", + ), + ExpressionTestCase( + "single_element_array", + expression={"$sigmoid": "$value"}, + doc={"value": [1]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject single-element array in insert (not treated as unary form)", + ), + ExpressionTestCase( + "composite_array_field", + expression={"$sigmoid": "$x.y"}, + doc={"x": [{"y": 1}, {"y": 2}]}, + error_code=TYPE_MISMATCH_ERROR, + msg="Should reject composite array from $x.y on array-of-objects", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SIGMOID_LITERAL_TESTS)) +def test_sigmoid_literal(collection, test): + """Test $sigmoid with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(SIGMOID_INSERT_TESTS)) +def test_sigmoid_insert(collection, test): + """Test $sigmoid with inserted document values""" + 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/arithmetic/sigmoid/test_sigmoid_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_numeric.py new file mode 100644 index 000000000..5ec5fc7a9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_numeric.py @@ -0,0 +1,278 @@ +""" +arithmetic $sigmoid tests. + +Core numeric type/value matrix for $sigmoid using both literal (inline) and +inserted document field arguments: zeros, unit values, and large-magnitude +saturation across int32, int64, double, and decimal128. +""" + +import pytest +from bson import ( + Decimal128, + Int64, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + DOUBLE_NEGATIVE_ZERO, + INT32_MAX, + INT32_MAX_MINUS_1, + INT32_MIN, + INT32_MIN_PLUS_1, + INT64_MAX, + INT64_MAX_MINUS_1, + INT64_MIN, + INT64_MIN_PLUS_1, +) + +SIGMOID_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_zero", + expression={"$sigmoid": 0}, + expected=0.5, + msg="sigmoid(0) = 0.5 (int32 input)", + ), + ExpressionTestCase( + "int64_zero", + expression={"$sigmoid": Int64(0)}, + expected=0.5, + msg="sigmoid(0) = 0.5 (int64 input)", + ), + ExpressionTestCase( + "double_zero", + expression={"$sigmoid": 0.0}, + expected=0.5, + msg="sigmoid(0.0) = 0.5", + ), + ExpressionTestCase( + "double_negative_zero", + expression={"$sigmoid": DOUBLE_NEGATIVE_ZERO}, + expected=0.5, + msg="sigmoid(-0.0) = 0.5", + ), + ExpressionTestCase( + "decimal_zero", + expression={"$sigmoid": Decimal128("0")}, + expected=Decimal128("0.5"), + msg="sigmoid(decimal 0) = 0.5", + ), + ExpressionTestCase( + "decimal_negative_zero", + expression={"$sigmoid": Decimal128("-0")}, + expected=Decimal128("0.5"), + msg="sigmoid(decimal -0) = 0.5", + ), + ExpressionTestCase( + "int32_one", + expression={"$sigmoid": 1}, + expected=pytest.approx(0.7310585786300049), + msg="sigmoid(1) ~= 0.7311 (int32 input)", + ), + ExpressionTestCase( + "int64_one", + expression={"$sigmoid": Int64(1)}, + expected=pytest.approx(0.7310585786300049), + msg="sigmoid(1) ~= 0.7311 (int64 input)", + ), + ExpressionTestCase( + "double_one", + expression={"$sigmoid": 1.0}, + expected=pytest.approx(0.7310585786300049), + msg="sigmoid(1.0) ~= 0.7311", + ), + ExpressionTestCase( + "decimal_one", + expression={"$sigmoid": Decimal128("1")}, + expected=Decimal128("0.7310585786300048792511592418218362"), + msg="sigmoid(decimal 1) to 34-digit precision", + ), + ExpressionTestCase( + "int32_neg_one", + expression={"$sigmoid": -1}, + expected=pytest.approx(0.2689414213699951), + msg="sigmoid(-1) ~= 0.2689 (int32 input)", + ), + ExpressionTestCase( + "int64_neg_one", + expression={"$sigmoid": Int64(-1)}, + expected=pytest.approx(0.2689414213699951), + msg="sigmoid(-1) ~= 0.2689 (int64 input)", + ), + ExpressionTestCase( + "double_neg_one", + expression={"$sigmoid": -1.0}, + expected=pytest.approx(0.2689414213699951), + msg="sigmoid(-1.0) ~= 0.2689", + ), + ExpressionTestCase( + "decimal_neg_one", + expression={"$sigmoid": Decimal128("-1")}, + expected=Decimal128("0.2689414213699951207488407581781638"), + msg="sigmoid(decimal -1) to 34-digit precision", + ), + ExpressionTestCase( + "int32_ten", + expression={"$sigmoid": 10}, + expected=pytest.approx(0.9999546021312976), + msg="sigmoid(10) ~= 0.99995", + ), + ExpressionTestCase( + "double_large_pos", + expression={"$sigmoid": 1e10}, + expected=1.0, + msg="large positive double (1e10) saturates to 1.0", + ), + ExpressionTestCase( + "int32_max", + expression={"$sigmoid": INT32_MAX}, + expected=1.0, + msg="int32 max saturates to 1.0", + ), + ExpressionTestCase( + "int32_max_minus_1", + expression={"$sigmoid": INT32_MAX_MINUS_1}, + expected=1.0, + msg="int32 max-1 saturates to 1.0", + ), + ExpressionTestCase( + "int64_max", + expression={"$sigmoid": INT64_MAX}, + expected=1.0, + msg="int64 max saturates to 1.0", + ), + ExpressionTestCase( + "int64_max_minus_1", + expression={"$sigmoid": INT64_MAX_MINUS_1}, + expected=1.0, + msg="int64 max-1 saturates to 1.0", + ), + ExpressionTestCase( + "int32_neg_ten", + expression={"$sigmoid": -10}, + expected=pytest.approx(0.000045397868702434395), + msg="sigmoid(-10) ~= 4.54e-5", + ), + ExpressionTestCase( + "double_large_neg", + expression={"$sigmoid": -1e10}, + expected=0.0, + msg="large negative double (-1e10) saturates to 0.0", + ), + ExpressionTestCase( + "int32_min", + expression={"$sigmoid": INT32_MIN}, + expected=0.0, + msg="int32 min saturates to 0.0", + ), + ExpressionTestCase( + "int32_min_plus_1", + expression={"$sigmoid": INT32_MIN_PLUS_1}, + expected=0.0, + msg="int32 min+1 saturates to 0.0", + ), + ExpressionTestCase( + "int64_min", + expression={"$sigmoid": INT64_MIN}, + expected=0.0, + msg="int64 min saturates to 0.0", + ), + ExpressionTestCase( + "int64_min_plus_1", + expression={"$sigmoid": INT64_MIN_PLUS_1}, + expected=0.0, + msg="int64 min+1 saturates to 0.0", + ), + ExpressionTestCase( + "double_min_subnormal", + expression={"$sigmoid": DOUBLE_MIN_SUBNORMAL}, + expected=0.5, + msg="smallest subnormal double ~= 0, sigmoid = 0.5", + ), + ExpressionTestCase( + "double_near_min", + expression={"$sigmoid": DOUBLE_NEAR_MIN}, + expected=0.5, + msg="tiny double (1e-308) ~= 0, sigmoid = 0.5", + ), + ExpressionTestCase( + "double_near_max", + expression={"$sigmoid": DOUBLE_NEAR_MAX}, + expected=1.0, + msg="huge double (1e308) saturates to 1.0", + ), + ExpressionTestCase( + "double_max_safe_integer", + expression={"$sigmoid": DOUBLE_MAX_SAFE_INTEGER}, + expected=1.0, + msg="2^53 max-safe-integer saturates to 1.0", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SIGMOID_LITERAL_TESTS)) +def test_sigmoid_literal(collection, test): + """Test $sigmoid with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +SIGMOID_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_one", + expression={"$sigmoid": "$value"}, + doc={"value": 1}, + expected=pytest.approx(0.7310585786300049), + msg="sigmoid(1) ~= 0.7311 (int32 input)", + ), + ExpressionTestCase( + "int64_neg_one", + expression={"$sigmoid": "$value"}, + doc={"value": Int64(-1)}, + expected=pytest.approx(0.2689414213699951), + msg="sigmoid(-1) ~= 0.2689 (int64 input)", + ), + ExpressionTestCase( + "double_zero", + expression={"$sigmoid": "$value"}, + doc={"value": 0.0}, + expected=0.5, + msg="sigmoid(0.0) = 0.5", + ), + ExpressionTestCase( + "decimal_one", + expression={"$sigmoid": "$value"}, + doc={"value": Decimal128("1")}, + expected=Decimal128("0.7310585786300048792511592418218362"), + msg="sigmoid(decimal 1) to 34-digit precision", + ), + ExpressionTestCase( + "int32_max", + expression={"$sigmoid": "$value"}, + doc={"value": INT32_MAX}, + expected=1.0, + msg="int32 max saturates to 1.0", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SIGMOID_INSERT_TESTS)) +def test_sigmoid_insert(collection, test): + """Test $sigmoid with inserted document values""" + 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/arithmetic/sigmoid/test_sigmoid_numeric_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_numeric_precision.py new file mode 100644 index 000000000..2ae1befd9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sigmoid/test_sigmoid_numeric_precision.py @@ -0,0 +1,421 @@ +""" +arithmetic $sigmoid tests. + +Precision and special-value matrix for $sigmoid using both literal (inline) +and inserted document field arguments: decimal128 extremes, half-value +precision for double and decimal128, and Infinity/NaN/null/missing handling. +""" + +import math + +import pytest +from bson import ( + Decimal128, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_HALF, + DECIMAL128_INFINITY, + DECIMAL128_JUST_ABOVE_HALF, + DECIMAL128_JUST_BELOW_HALF, + DECIMAL128_LARGE_EXPONENT, + DECIMAL128_MANY_TRAILING_ZEROS, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_HALF, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ONE_AND_HALF, + DECIMAL128_ONE_AND_HALF, + DECIMAL128_SMALL_EXPONENT, + DECIMAL128_TRAILING_ZERO, + DECIMAL128_TWO_AND_HALF, + DOUBLE_HALF, + DOUBLE_JUST_ABOVE_HALF, + DOUBLE_JUST_BELOW_HALF, + DOUBLE_NEGATIVE_HALF, + DOUBLE_NEGATIVE_ONE_AND_HALF, + DOUBLE_ONE_AND_HALF, + DOUBLE_TWO_AND_HALF, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + MISSING, +) + +SIGMOID_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal128_max", + expression={"$sigmoid": DECIMAL128_MAX}, + expected=Decimal128("1"), + msg="decimal128 max saturates to 1", + ), + ExpressionTestCase( + "decimal128_min", + expression={"$sigmoid": DECIMAL128_MIN}, + expected=Decimal128("0E-6176"), + msg="decimal128 min saturates to 0E-6176", + ), + ExpressionTestCase( + "decimal128_small_exponent", + expression={"$sigmoid": DECIMAL128_SMALL_EXPONENT}, + expected=Decimal128("0.5"), + msg="tiny decimal (1E-6143) ~= 0, sigmoid = 0.5", + ), + ExpressionTestCase( + "decimal128_large_exponent", + expression={"$sigmoid": DECIMAL128_LARGE_EXPONENT}, + expected=Decimal128("1"), + msg="huge decimal (1E+6144) saturates to 1", + ), + ExpressionTestCase( + "decimal128_trailing_zero", + expression={"$sigmoid": DECIMAL128_TRAILING_ZERO}, + expected=Decimal128("0.7310585786300048792511592418218362"), + msg="decimal 1.0 -> sigmoid(1) to 34-digit precision", + ), + ExpressionTestCase( + "decimal128_many_trailing_zeros", + expression={"$sigmoid": DECIMAL128_MANY_TRAILING_ZEROS}, + expected=Decimal128("0.7310585786300048792511592418218362"), + msg="decimal 1.000... -> sigmoid(1) to 34-digit precision", + ), + ExpressionTestCase( + "double_half", + expression={"$sigmoid": DOUBLE_HALF}, + expected=pytest.approx(0.6224593312018546), + msg="sigmoid(0.5) ~= 0.62246", + ), + ExpressionTestCase( + "double_one_and_half", + expression={"$sigmoid": DOUBLE_ONE_AND_HALF}, + expected=pytest.approx(0.8175744761936437), + msg="sigmoid(1.5) ~= 0.81757", + ), + ExpressionTestCase( + "double_two_and_half", + expression={"$sigmoid": DOUBLE_TWO_AND_HALF}, + expected=pytest.approx(0.9241418199787566), + msg="sigmoid(2.5) ~= 0.92414", + ), + ExpressionTestCase( + "double_negative_half", + expression={"$sigmoid": DOUBLE_NEGATIVE_HALF}, + expected=pytest.approx(0.3775406687981454), + msg="sigmoid(-0.5) ~= 0.37754", + ), + ExpressionTestCase( + "double_negative_one_and_half", + expression={"$sigmoid": DOUBLE_NEGATIVE_ONE_AND_HALF}, + expected=pytest.approx(0.18242552380635635), + msg="sigmoid(-1.5) ~= 0.18243", + ), + ExpressionTestCase( + "double_just_below_half", + expression={"$sigmoid": DOUBLE_JUST_BELOW_HALF}, + expected=pytest.approx(0.6224593312018546), + msg="double just below 0.5 rounds to sigmoid(0.5) ~= 0.62246", + ), + ExpressionTestCase( + "double_just_above_half", + expression={"$sigmoid": DOUBLE_JUST_ABOVE_HALF}, + expected=pytest.approx(0.6224593314368583), + msg="double just above 0.5 ~= 0.62246 (differs from sigmoid(0.5) past double precision)", + ), + ExpressionTestCase( + "decimal_half", + expression={"$sigmoid": DECIMAL128_HALF}, + expected=Decimal128("0.6224593312018545646389005657455087"), + msg="sigmoid(decimal 0.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_one_and_half", + expression={"$sigmoid": DECIMAL128_ONE_AND_HALF}, + expected=Decimal128("0.8175744761936436596072171786562486"), + msg="sigmoid(decimal 1.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_two_and_half", + expression={"$sigmoid": DECIMAL128_TWO_AND_HALF}, + expected=Decimal128("0.9241418199787564488066938233537521"), + msg="sigmoid(decimal 2.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_negative_half", + expression={"$sigmoid": DECIMAL128_NEGATIVE_HALF}, + expected=Decimal128("0.3775406687981454353610994342544915"), + msg="sigmoid(decimal -0.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_negative_one_and_half", + expression={"$sigmoid": DECIMAL128_NEGATIVE_ONE_AND_HALF}, + expected=Decimal128("0.1824255238063563403927828213437517"), + msg="sigmoid(decimal -1.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_just_below_half", + expression={"$sigmoid": DECIMAL128_JUST_BELOW_HALF}, + expected=Decimal128("0.6224593312018545646389005657455083"), + msg="34-digit decimal just below 0.5 (last digit differs from the 0.5 result)", + ), + ExpressionTestCase( + "decimal_just_above_half", + expression={"$sigmoid": DECIMAL128_JUST_ABOVE_HALF}, + expected=Decimal128("0.6224593312018545646389005657455087"), + msg="34-digit decimal just above 0.5 (matches the 0.5 result at 34-digit precision)", + ), + ExpressionTestCase( + "float_infinity", + expression={"$sigmoid": FLOAT_INFINITY}, + expected=1.0, + msg="sigmoid(+Infinity) = 1.0", + ), + ExpressionTestCase( + "float_negative_infinity", + expression={"$sigmoid": FLOAT_NEGATIVE_INFINITY}, + expected=0.0, + msg="sigmoid(-Infinity) = 0.0", + ), + ExpressionTestCase( + "decimal128_infinity", + expression={"$sigmoid": DECIMAL128_INFINITY}, + expected=Decimal128("1"), + msg="sigmoid(decimal +Infinity) = 1", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + expression={"$sigmoid": DECIMAL128_NEGATIVE_INFINITY}, + expected=Decimal128("0E-6176"), + msg="sigmoid(decimal -Infinity) = 0E-6176", + ), + ExpressionTestCase( + "float_nan", + expression={"$sigmoid": FLOAT_NAN}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="sigmoid(NaN) = NaN", + ), + ExpressionTestCase( + "decimal128_nan", + expression={"$sigmoid": DECIMAL128_NAN}, + expected=DECIMAL128_NAN, + msg="sigmoid(decimal NaN) = NaN", + ), + ExpressionTestCase( + "null_value", + expression={"$sigmoid": None}, + expected=None, + msg="null input returns null", + ), + ExpressionTestCase( + "missing_value", + expression={"$sigmoid": MISSING}, + expected=None, + msg="missing input returns null", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SIGMOID_LITERAL_TESTS)) +def test_sigmoid_literal(collection, test): + """Test $sigmoid with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +SIGMOID_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal128_max", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_MAX}, + expected=Decimal128("1"), + msg="decimal128 max saturates to 1", + ), + ExpressionTestCase( + "decimal128_min", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_MIN}, + expected=Decimal128("0E-6176"), + msg="decimal128 min saturates to 0E-6176", + ), + ExpressionTestCase( + "decimal128_small_exponent", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_SMALL_EXPONENT}, + expected=Decimal128("0.5"), + msg="tiny decimal (1E-6143) ~= 0, sigmoid = 0.5", + ), + ExpressionTestCase( + "decimal128_large_exponent", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_LARGE_EXPONENT}, + expected=Decimal128("1"), + msg="huge decimal (1E+6144) saturates to 1", + ), + ExpressionTestCase( + "decimal128_trailing_zero", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_TRAILING_ZERO}, + expected=Decimal128("0.7310585786300048792511592418218362"), + msg="decimal 1.0 -> sigmoid(1) to 34-digit precision", + ), + ExpressionTestCase( + "decimal128_many_trailing_zeros", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_MANY_TRAILING_ZEROS}, + expected=Decimal128("0.7310585786300048792511592418218362"), + msg="decimal 1.000... -> sigmoid(1) to 34-digit precision", + ), + ExpressionTestCase( + "double_half", + expression={"$sigmoid": "$value"}, + doc={"value": DOUBLE_HALF}, + expected=pytest.approx(0.6224593312018546), + msg="sigmoid(0.5) ~= 0.62246", + ), + ExpressionTestCase( + "double_one_and_half", + expression={"$sigmoid": "$value"}, + doc={"value": DOUBLE_ONE_AND_HALF}, + expected=pytest.approx(0.8175744761936437), + msg="sigmoid(1.5) ~= 0.81757", + ), + ExpressionTestCase( + "double_two_and_half", + expression={"$sigmoid": "$value"}, + doc={"value": DOUBLE_TWO_AND_HALF}, + expected=pytest.approx(0.9241418199787566), + msg="sigmoid(2.5) ~= 0.92414", + ), + ExpressionTestCase( + "double_negative_half", + expression={"$sigmoid": "$value"}, + doc={"value": DOUBLE_NEGATIVE_HALF}, + expected=pytest.approx(0.3775406687981454), + msg="sigmoid(-0.5) ~= 0.37754", + ), + ExpressionTestCase( + "double_negative_one_and_half", + expression={"$sigmoid": "$value"}, + doc={"value": DOUBLE_NEGATIVE_ONE_AND_HALF}, + expected=pytest.approx(0.18242552380635635), + msg="sigmoid(-1.5) ~= 0.18243", + ), + ExpressionTestCase( + "double_just_below_half", + expression={"$sigmoid": "$value"}, + doc={"value": DOUBLE_JUST_BELOW_HALF}, + expected=pytest.approx(0.6224593312018546), + msg="double just below 0.5 rounds to sigmoid(0.5) ~= 0.62246", + ), + ExpressionTestCase( + "decimal_half", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_HALF}, + expected=Decimal128("0.6224593312018545646389005657455087"), + msg="sigmoid(decimal 0.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_one_and_half", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_ONE_AND_HALF}, + expected=Decimal128("0.8175744761936436596072171786562486"), + msg="sigmoid(decimal 1.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_two_and_half", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_TWO_AND_HALF}, + expected=Decimal128("0.9241418199787564488066938233537521"), + msg="sigmoid(decimal 2.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_negative_half", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_NEGATIVE_HALF}, + expected=Decimal128("0.3775406687981454353610994342544915"), + msg="sigmoid(decimal -0.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_negative_one_and_half", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_NEGATIVE_ONE_AND_HALF}, + expected=Decimal128("0.1824255238063563403927828213437517"), + msg="sigmoid(decimal -1.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_just_below_half", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_JUST_BELOW_HALF}, + expected=Decimal128("0.6224593312018545646389005657455083"), + msg="34-digit decimal just below 0.5 (last digit differs from the 0.5 result)", + ), + ExpressionTestCase( + "float_infinity", + expression={"$sigmoid": "$value"}, + doc={"value": FLOAT_INFINITY}, + expected=1.0, + msg="sigmoid(+Infinity) = 1.0", + ), + ExpressionTestCase( + "float_negative_infinity", + expression={"$sigmoid": "$value"}, + doc={"value": FLOAT_NEGATIVE_INFINITY}, + expected=0.0, + msg="sigmoid(-Infinity) = 0.0", + ), + ExpressionTestCase( + "decimal128_infinity", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_INFINITY}, + expected=Decimal128("1"), + msg="sigmoid(decimal +Infinity) = 1", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_NEGATIVE_INFINITY}, + expected=Decimal128("0E-6176"), + msg="sigmoid(decimal -Infinity) = 0E-6176", + ), + ExpressionTestCase( + "float_nan", + expression={"$sigmoid": "$value"}, + doc={"value": FLOAT_NAN}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="sigmoid(NaN) = NaN", + ), + ExpressionTestCase( + "decimal128_nan", + expression={"$sigmoid": "$value"}, + doc={"value": DECIMAL128_NAN}, + expected=DECIMAL128_NAN, + msg="sigmoid(decimal NaN) = NaN", + ), + ExpressionTestCase( + "null_value", + expression={"$sigmoid": "$value"}, + doc={"value": None}, + expected=None, + msg="null input returns null", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SIGMOID_INSERT_TESTS)) +def test_sigmoid_insert(collection, test): + """Test $sigmoid with inserted document values""" + 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/arithmetic/sqrt/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_basic.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_basic.py new file mode 100644 index 000000000..cf4e3a26a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_basic.py @@ -0,0 +1,311 @@ +""" +arithmetic $sqrt tests. + +Tests for the arithmetic $sqrt operator: zero, perfect squares, and simple +decimal values, as literal expressions and as inserted document fields +(a representative subset). +""" + +import pytest +from bson import ( + Decimal128, + Int64, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +SQRT_BASIC_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_zero", + expression={"$sqrt": 0}, + expected=0.0, + msg="sqrt(0) = 0.0 (int32 input returns double)", + ), + ExpressionTestCase( + "double_zero", + expression={"$sqrt": 0.0}, + expected=0.0, + msg="sqrt(0.0) = 0.0", + ), + ExpressionTestCase( + "double_negative_zero", + expression={"$sqrt": -0.0}, + expected=-0.0, + msg="sqrt(-0.0) = -0.0 (preserves negative-zero sign)", + ), + ExpressionTestCase( + "decimal_zero", + expression={"$sqrt": Decimal128("0")}, + expected=Decimal128("0"), + msg="sqrt(decimal 0) = 0 (type preserved)", + ), + ExpressionTestCase( + "decimal_negative_zero", + expression={"$sqrt": Decimal128("-0")}, + expected=Decimal128("-0"), + msg="sqrt(decimal -0) = -0 (preserves negative-zero sign)", + ), + ExpressionTestCase( + "int32_one", + expression={"$sqrt": 1}, + expected=1.0, + msg="sqrt(1) = 1.0", + ), + ExpressionTestCase( + "int32_four", + expression={"$sqrt": 4}, + expected=2.0, + msg="sqrt(4) = 2.0 (int32 input)", + ), + ExpressionTestCase( + "int32_nine", + expression={"$sqrt": 9}, + expected=3.0, + msg="sqrt(9) = 3.0", + ), + ExpressionTestCase( + "int32_sixteen", + expression={"$sqrt": 16}, + expected=4.0, + msg="sqrt(16) = 4.0", + ), + ExpressionTestCase( + "int32_hundred", + expression={"$sqrt": 100}, + expected=10.0, + msg="sqrt(100) = 10.0", + ), + ExpressionTestCase( + "int64_four", + expression={"$sqrt": Int64(4)}, + expected=2.0, + msg="sqrt(4) = 2.0 (int64 input returns double)", + ), + ExpressionTestCase( + "int64_hundred", + expression={"$sqrt": Int64(100)}, + expected=10.0, + msg="sqrt(100) = 10.0 (int64 input)", + ), + ExpressionTestCase( + "double_four", + expression={"$sqrt": 4.0}, + expected=2.0, + msg="sqrt(4.0) = 2.0 (double input)", + ), + ExpressionTestCase( + "double_quarter", + expression={"$sqrt": 0.25}, + expected=0.5, + msg="sqrt(0.25) = 0.5", + ), + ExpressionTestCase( + "double_0_01", + expression={"$sqrt": 0.01}, + expected=0.1, + msg="sqrt(0.01) = 0.1", + ), + ExpressionTestCase( + "decimal_four", + expression={"$sqrt": Decimal128("4")}, + expected=Decimal128("2"), + msg="sqrt(decimal 4) = 2 (decimal type preserved)", + ), + ExpressionTestCase( + "decimal_nine", + expression={"$sqrt": Decimal128("9")}, + expected=Decimal128("3"), + msg="sqrt(decimal 9) = 3", + ), + ExpressionTestCase( + "decimal_quarter", + expression={"$sqrt": Decimal128("0.25")}, + expected=Decimal128("0.5"), + msg="sqrt(decimal 0.25) = 0.5", + ), + ExpressionTestCase( + "int32_two", + expression={"$sqrt": 2}, + expected=pytest.approx(1.4142135623730951), + msg="sqrt(2) ~= 1.41421 (irrational, double precision)", + ), + ExpressionTestCase( + "int32_three", + expression={"$sqrt": 3}, + expected=pytest.approx(1.7320508075688772), + msg="sqrt(3) ~= 1.73205 (irrational)", + ), + ExpressionTestCase( + "int32_five", + expression={"$sqrt": 5}, + expected=pytest.approx(2.23606797749979), + msg="sqrt(5) ~= 2.23607 (irrational)", + ), + ExpressionTestCase( + "decimal_two", + expression={"$sqrt": Decimal128("2")}, + expected=Decimal128("1.414213562373095048801688724209698"), + msg="sqrt(decimal 2) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_three", + expression={"$sqrt": Decimal128("3")}, + expected=Decimal128("1.732050807568877293527446341505872"), + msg="sqrt(decimal 3) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_five", + expression={"$sqrt": Decimal128("5")}, + expected=Decimal128("2.236067977499789696409173668731276"), + msg="sqrt(decimal 5) to 34-digit precision", + ), + ExpressionTestCase( + "double_0_04", + expression={"$sqrt": 0.04}, + expected=0.2, + msg="sqrt(0.04) = 0.2", + ), + ExpressionTestCase( + "double_0_09", + expression={"$sqrt": 0.09}, + expected=0.3, + msg="sqrt(0.09) = 0.3", + ), + ExpressionTestCase( + "double_0_16", + expression={"$sqrt": 0.16}, + expected=0.4, + msg="sqrt(0.16) = 0.4", + ), + ExpressionTestCase( + "double_0_36", + expression={"$sqrt": 0.36}, + expected=0.6, + msg="sqrt(0.36) = 0.6", + ), + ExpressionTestCase( + "double_0_49", + expression={"$sqrt": 0.49}, + expected=0.7, + msg="sqrt(0.49) = 0.7", + ), + ExpressionTestCase( + "double_0_64", + expression={"$sqrt": 0.64}, + expected=0.8, + msg="sqrt(0.64) = 0.8", + ), + ExpressionTestCase( + "double_0_81", + expression={"$sqrt": 0.81}, + expected=0.9, + msg="sqrt(0.81) = 0.9", + ), + ExpressionTestCase( + "decimal_0_04", + expression={"$sqrt": Decimal128("0.04")}, + expected=Decimal128("0.2"), + msg="sqrt(decimal 0.04) = 0.2", + ), + ExpressionTestCase( + "decimal_0_09", + expression={"$sqrt": Decimal128("0.09")}, + expected=Decimal128("0.3"), + msg="sqrt(decimal 0.09) = 0.3", + ), + ExpressionTestCase( + "decimal_0_16", + expression={"$sqrt": Decimal128("0.16")}, + expected=Decimal128("0.4"), + msg="sqrt(decimal 0.16) = 0.4", + ), + ExpressionTestCase( + "decimal_0_36", + expression={"$sqrt": Decimal128("0.36")}, + expected=Decimal128("0.6"), + msg="sqrt(decimal 0.36) = 0.6", + ), + ExpressionTestCase( + "decimal_0_49", + expression={"$sqrt": Decimal128("0.49")}, + expected=Decimal128("0.7"), + msg="sqrt(decimal 0.49) = 0.7", + ), + ExpressionTestCase( + "decimal_0_64", + expression={"$sqrt": Decimal128("0.64")}, + expected=Decimal128("0.8"), + msg="sqrt(decimal 0.64) = 0.8", + ), + ExpressionTestCase( + "decimal_0_81", + expression={"$sqrt": Decimal128("0.81")}, + expected=Decimal128("0.9"), + msg="sqrt(decimal 0.81) = 0.9", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SQRT_BASIC_LITERAL_TESTS)) +def test_sqrt_basic_literal(collection, test): + """Test $sqrt with literal zero, perfect-square, and simple decimal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +SQRT_BASIC_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_four", + expression={"$sqrt": "$value"}, + doc={"value": 4}, + expected=2.0, + msg="sqrt(4) = 2.0 (int32 input returns double)", + ), + ExpressionTestCase( + "int64_hundred", + expression={"$sqrt": "$value"}, + doc={"value": Int64(100)}, + expected=10.0, + msg="sqrt(100) = 10.0 (int64 input)", + ), + ExpressionTestCase( + "double_quarter", + expression={"$sqrt": "$value"}, + doc={"value": 0.25}, + expected=0.5, + msg="sqrt(0.25) = 0.5 (double input)", + ), + ExpressionTestCase( + "decimal_nine", + expression={"$sqrt": "$value"}, + doc={"value": Decimal128("9")}, + expected=Decimal128("3"), + msg="sqrt(decimal 9) = 3 (decimal type preserved)", + ), + ExpressionTestCase( + "double_negative_zero", + expression={"$sqrt": "$value"}, + doc={"value": -0.0}, + expected=-0.0, + msg="sqrt(-0.0) = -0.0 (preserves negative-zero sign, via inserted field)", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SQRT_BASIC_INSERT_TESTS)) +def test_sqrt_basic_insert(collection, test): + """Test $sqrt with inserted zero, perfect-square, and simple decimal values""" + 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/arithmetic/sqrt/test_sqrt_bson_type_validation.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_bson_type_validation.py new file mode 100644 index 000000000..f5bad22e3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_bson_type_validation.py @@ -0,0 +1,63 @@ +""" +BSON type validation tests for $sqrt expression. + +Systematically verifies that the single numeric input accepts the four numeric +types plus null, and rejects every other BSON type. Samples are non-negative so +acceptance does not trip the negative-input error. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertNotError +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import NON_NUMERIC_TYPE_MISMATCH_ERROR + +SQRT_BSON_PARAMS = [ + BsonTypeTestCase( + id="input", + msg="$sqrt should reject non-numeric input", + keyword="input", + valid_types=[ + BsonType.DOUBLE, + BsonType.INT, + BsonType.LONG, + BsonType.DECIMAL, + BsonType.NULL, + ], + default_error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + ), +] + +REJECTION_CASES = generate_bson_rejection_test_cases(SQRT_BSON_PARAMS) +ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(SQRT_BSON_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", REJECTION_CASES) +def test_sqrt_bson_type_rejected(collection, bson_type, sample_value, spec): + """Verifies $sqrt rejects invalid BSON types.""" + result = execute_expression_with_insert( + collection, {"$sqrt": "$value"}, {"value": sample_value} + ) + assert_expression_result( + result, + error_code=spec.expected_code(bson_type), + msg=f"{spec.msg}: {bson_type.value}", + ) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) +def test_sqrt_bson_type_accepted(collection, bson_type, sample_value, spec): + """Verifies $sqrt accepts valid numeric BSON types (and null).""" + result = execute_expression_with_insert( + collection, {"$sqrt": "$value"}, {"value": sample_value} + ) + assertNotError(result, msg=f"$sqrt should accept {bson_type.value}") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_composition.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_composition.py new file mode 100644 index 000000000..d602e63d1 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_composition.py @@ -0,0 +1,93 @@ +""" +arithmetic $sqrt tests. + +Self-nested $sqrt expressions and field-path lookup tests for $sqrt +(simple/nested/deep/nonexistent paths). +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import NON_NUMERIC_TYPE_MISMATCH_ERROR +from documentdb_tests.framework.parametrize import pytest_params + +SQRT_NESTED_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_sqrt_2", + expression={"$sqrt": {"$sqrt": 16}}, + expected=2.0, + msg="sqrt(sqrt(16)) = 2.0 (double self-nesting)", + ), + ExpressionTestCase( + "nested_sqrt_2_large", + expression={"$sqrt": {"$sqrt": 65536}}, + expected=16.0, + msg="sqrt(sqrt(65536)) = 16.0 (self-nesting, large input)", + ), + ExpressionTestCase( + "nested_sqrt_3", + expression={"$sqrt": {"$sqrt": {"$sqrt": 256}}}, + expected=2.0, + msg="sqrt(sqrt(sqrt(256))) = 2.0 (triple self-nesting)", + ), +] + + +SQRT_FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_field", + expression={"$sqrt": "$a.b"}, + doc={"a": {"b": 16}}, + expected=4.0, + msg="sqrt(16) = 4.0 resolved via 1-level field path $a.b", + ), + ExpressionTestCase( + "nonexistent_field", + expression={"$sqrt": "$a.nonexistent"}, + doc={"a": {"missing": 1}}, + expected=None, + msg="nonexistent field path returns null", + ), + ExpressionTestCase( + "deeply_nested_field", + expression={"$sqrt": "$a.b.c.d"}, + doc={"a": {"b": {"c": {"d": 25}}}}, + expected=5.0, + msg="sqrt(25) = 5.0 resolved via deep field path $a.b.c.d", + ), + ExpressionTestCase( + "array_index_field_path", + expression={"$sqrt": "$a.0"}, + doc={"a": [4, 9]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg=( + "$sqrt should reject a numeric path component over an array, " + "which resolves non-numeric" + ), + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SQRT_NESTED_TESTS)) +def test_sqrt_nested_expression(collection, test): + """Test $sqrt self-nesting (double and triple nested)""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(SQRT_FIELD_LOOKUP_TESTS)) +def test_sqrt_field_lookup(collection, test): + """Test $sqrt with simple, nested, deep, and nonexistent field paths""" + 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/arithmetic/sqrt/test_sqrt_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_errors.py new file mode 100644 index 000000000..218f541e3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_errors.py @@ -0,0 +1,302 @@ +""" +arithmetic $sqrt tests. + +Tests for the arithmetic $sqrt operator: negative-input errors (all numeric +types, including boundary and infinity values), non-numeric type-mismatch +errors, and arity errors (empty/multi-element array literals), as both literal +and inserted document field values. Also covers composite-array field +rejection. +""" + +import pytest +from bson import ( + Decimal128, + Int64, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + EXPRESSION_TYPE_MISMATCH_ERROR, + NON_NUMERIC_TYPE_MISMATCH_ERROR, + SQRT_NEGATIVE_INPUT_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_MIN, + DECIMAL128_NEGATIVE_HALF, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ONE_AND_HALF, + DOUBLE_NEGATIVE_HALF, + DOUBLE_NEGATIVE_ONE_AND_HALF, + FLOAT_NEGATIVE_INFINITY, + INT32_MIN, + INT32_MIN_PLUS_1, + INT64_MIN, + INT64_MIN_PLUS_1, +) + +SQRT_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "negative_one", + expression={"$sqrt": -1}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject negative one", + ), + ExpressionTestCase( + "negative_int32_min", + expression={"$sqrt": INT32_MIN}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject int32 min", + ), + ExpressionTestCase( + "negative_int32_min_plus_1", + expression={"$sqrt": INT32_MIN_PLUS_1}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject int32 min plus 1", + ), + ExpressionTestCase( + "negative_int64", + expression={"$sqrt": Int64(-4)}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject negative int64", + ), + ExpressionTestCase( + "negative_int64_min", + expression={"$sqrt": INT64_MIN}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject int64 min", + ), + ExpressionTestCase( + "negative_int64_min_plus_1", + expression={"$sqrt": INT64_MIN_PLUS_1}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject int64 min plus 1", + ), + ExpressionTestCase( + "negative_double", + expression={"$sqrt": -2.5}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject negative double", + ), + ExpressionTestCase( + "negative_decimal", + expression={"$sqrt": Decimal128("-1")}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject negative decimal", + ), + ExpressionTestCase( + "double_negative_half", + expression={"$sqrt": DOUBLE_NEGATIVE_HALF}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject double negative half", + ), + ExpressionTestCase( + "double_negative_one_and_half", + expression={"$sqrt": DOUBLE_NEGATIVE_ONE_AND_HALF}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject double negative one and half", + ), + ExpressionTestCase( + "decimal_negative_half", + expression={"$sqrt": DECIMAL128_NEGATIVE_HALF}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject decimal negative half", + ), + ExpressionTestCase( + "decimal_negative_one_and_half", + expression={"$sqrt": DECIMAL128_NEGATIVE_ONE_AND_HALF}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject decimal negative one and half", + ), + ExpressionTestCase( + "decimal128_min", + expression={"$sqrt": DECIMAL128_MIN}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject decimal128 min", + ), + ExpressionTestCase( + "float_negative_infinity", + expression={"$sqrt": FLOAT_NEGATIVE_INFINITY}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject float negative infinity", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + expression={"$sqrt": DECIMAL128_NEGATIVE_INFINITY}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject decimal128 negative infinity", + ), + ExpressionTestCase( + "empty_object", + expression={"$sqrt": {}}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="Should reject empty object", + ), + ExpressionTestCase( + "array_value", + expression={"$sqrt": [1, 2]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="Should reject array value in literal", + ), + ExpressionTestCase( + "empty_array", + expression={"$sqrt": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="Should reject empty array in literal", + ), +] + + +SQRT_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "negative_one", + expression={"$sqrt": "$value"}, + doc={"value": -1}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject negative one", + ), + ExpressionTestCase( + "negative_int32_min", + expression={"$sqrt": "$value"}, + doc={"value": INT32_MIN}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject int32 min", + ), + ExpressionTestCase( + "negative_int32_min_plus_1", + expression={"$sqrt": "$value"}, + doc={"value": INT32_MIN_PLUS_1}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject int32 min plus 1", + ), + ExpressionTestCase( + "negative_int64", + expression={"$sqrt": "$value"}, + doc={"value": Int64(-4)}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject negative int64", + ), + ExpressionTestCase( + "negative_int64_min", + expression={"$sqrt": "$value"}, + doc={"value": INT64_MIN}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject int64 min", + ), + ExpressionTestCase( + "negative_int64_min_plus_1", + expression={"$sqrt": "$value"}, + doc={"value": INT64_MIN_PLUS_1}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject int64 min plus 1", + ), + ExpressionTestCase( + "negative_double", + expression={"$sqrt": "$value"}, + doc={"value": -2.5}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject negative double", + ), + ExpressionTestCase( + "negative_decimal", + expression={"$sqrt": "$value"}, + doc={"value": Decimal128("-1")}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject negative decimal", + ), + ExpressionTestCase( + "double_negative_half", + expression={"$sqrt": "$value"}, + doc={"value": DOUBLE_NEGATIVE_HALF}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject double negative half", + ), + ExpressionTestCase( + "double_negative_one_and_half", + expression={"$sqrt": "$value"}, + doc={"value": DOUBLE_NEGATIVE_ONE_AND_HALF}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject double negative one and half", + ), + ExpressionTestCase( + "decimal_negative_half", + expression={"$sqrt": "$value"}, + doc={"value": DECIMAL128_NEGATIVE_HALF}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject decimal negative half", + ), + ExpressionTestCase( + "decimal_negative_one_and_half", + expression={"$sqrt": "$value"}, + doc={"value": DECIMAL128_NEGATIVE_ONE_AND_HALF}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject decimal negative one and half", + ), + ExpressionTestCase( + "decimal128_min", + expression={"$sqrt": "$value"}, + doc={"value": DECIMAL128_MIN}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject decimal128 min", + ), + ExpressionTestCase( + "float_negative_infinity", + expression={"$sqrt": "$value"}, + doc={"value": FLOAT_NEGATIVE_INFINITY}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject float negative infinity", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + expression={"$sqrt": "$value"}, + doc={"value": DECIMAL128_NEGATIVE_INFINITY}, + error_code=SQRT_NEGATIVE_INPUT_ERROR, + msg="Should reject decimal128 negative infinity", + ), + ExpressionTestCase( + "empty_object", + expression={"$sqrt": "$value"}, + doc={"value": {}}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="Should reject empty object", + ), + ExpressionTestCase( + "empty_array_field", + expression={"$sqrt": "$value"}, + doc={"value": []}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="Empty-array field value is non-numeric (not an arity error)", + ), + ExpressionTestCase( + "composite_array_field", + expression={"$sqrt": "$x.y"}, + doc={"x": [{"y": 4}, {"y": 9}]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="Should reject composite array from $x.y on array-of-objects", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SQRT_LITERAL_TESTS)) +def test_sqrt_literal(collection, test): + """Test $sqrt with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(SQRT_INSERT_TESTS)) +def test_sqrt_insert(collection, test): + """Test $sqrt with inserted document values""" + 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/arithmetic/sqrt/test_sqrt_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_precision.py new file mode 100644 index 000000000..95fc5369b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/sqrt/test_sqrt_precision.py @@ -0,0 +1,360 @@ +""" +arithmetic $sqrt tests. + +Tests for the arithmetic $sqrt operator: non-perfect-square values, numeric +boundaries, decimal128 precision, special values (NaN/Infinity/null/missing), +and the single-element array wrapper, as literal expressions and as +inserted document fields (a reduced-but-present subset). +""" + +import math + +import pytest +from bson import Decimal128 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_HALF, + DECIMAL128_INFINITY, + DECIMAL128_JUST_ABOVE_HALF, + DECIMAL128_LARGE_EXPONENT, + DECIMAL128_MANY_TRAILING_ZEROS, + DECIMAL128_MAX, + DECIMAL128_MIN_POSITIVE, + DECIMAL128_NAN, + DECIMAL128_ONE_AND_HALF, + DECIMAL128_SMALL_EXPONENT, + DECIMAL128_TRAILING_ZERO, + DECIMAL128_TWO_AND_HALF, + DOUBLE_HALF, + DOUBLE_JUST_ABOVE_HALF, + DOUBLE_JUST_BELOW_HALF, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + DOUBLE_ONE_AND_HALF, + DOUBLE_TWO_AND_HALF, + FLOAT_INFINITY, + FLOAT_NAN, + INT32_MAX, + INT32_MAX_MINUS_1, + INT64_MAX, + INT64_MAX_MINUS_1, + MISSING, +) + +SQRT_PRECISION_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_ten", + expression={"$sqrt": 10}, + expected=pytest.approx(3.1622776601683795), + msg="sqrt(10) ~= 3.16228 (irrational)", + ), + ExpressionTestCase( + "int32_thousand", + expression={"$sqrt": 1000}, + expected=pytest.approx(31.622776601683793), + msg="sqrt(1000) ~= 31.6228", + ), + ExpressionTestCase( + "double_0_1", + expression={"$sqrt": 0.1}, + expected=pytest.approx(0.31622776601683794), + msg="sqrt(0.1) ~= 0.316228", + ), + ExpressionTestCase( + "double_0_001", + expression={"$sqrt": 0.001}, + expected=pytest.approx(0.03162277660168379), + msg="sqrt(0.001) ~= 0.0316228", + ), + ExpressionTestCase( + "decimal_ten", + expression={"$sqrt": Decimal128("10")}, + expected=Decimal128("3.162277660168379331998893544432719"), + msg="sqrt(decimal 10) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_thousand", + expression={"$sqrt": Decimal128("1000")}, + expected=Decimal128("31.62277660168379331998893544432719"), + msg="sqrt(decimal 1000) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_0_1", + expression={"$sqrt": Decimal128("0.1")}, + expected=Decimal128("0.3162277660168379331998893544432719"), + msg="sqrt(decimal 0.1) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_0_001", + expression={"$sqrt": Decimal128("0.001")}, + expected=Decimal128("0.03162277660168379331998893544432719"), + msg="sqrt(decimal 0.001) to 34-digit precision", + ), + ExpressionTestCase( + "double_pi", + expression={"$sqrt": math.pi}, + expected=pytest.approx(1.7724538509055159), + msg="sqrt(pi) ~= 1.77245", + ), + ExpressionTestCase( + "double_e", + expression={"$sqrt": math.e}, + expected=pytest.approx(1.6487212707001282), + msg="sqrt(e) ~= 1.64872", + ), + ExpressionTestCase( + "int32_max", + expression={"$sqrt": INT32_MAX}, + expected=pytest.approx(46340.95001), + msg="sqrt(INT32_MAX) ~= 46340.95", + ), + ExpressionTestCase( + "int32_max_minus_1", + expression={"$sqrt": INT32_MAX_MINUS_1}, + expected=pytest.approx(46340.95001), + msg="sqrt(INT32_MAX-1) ~= 46340.95 (adjacent to max)", + ), + ExpressionTestCase( + "int64_max", + expression={"$sqrt": INT64_MAX}, + expected=pytest.approx(3.0370004999760455e9), + msg="sqrt(INT64_MAX) ~= 3.037e9", + ), + ExpressionTestCase( + "int64_max_minus_1", + expression={"$sqrt": INT64_MAX_MINUS_1}, + expected=pytest.approx(3.0370004999760455e9), + msg="sqrt(INT64_MAX-1) ~= 3.037e9 (adjacent to max)", + ), + ExpressionTestCase( + "double_min_subnormal", + expression={"$sqrt": DOUBLE_MIN_SUBNORMAL}, + expected=pytest.approx(2.2250738585072014e-162), + msg="sqrt of smallest subnormal double ~= 2.225e-162", + ), + ExpressionTestCase( + "double_near_min", + expression={"$sqrt": DOUBLE_NEAR_MIN}, + expected=pytest.approx(3.162277660168379e-154), + msg="sqrt(1e-308) ~= 3.16e-154", + ), + ExpressionTestCase( + "double_near_max", + expression={"$sqrt": DOUBLE_NEAR_MAX}, + expected=1e154, + msg="sqrt(1e308) = 1e154", + ), + ExpressionTestCase( + "double_max_safe_integer", + expression={"$sqrt": DOUBLE_MAX_SAFE_INTEGER}, + expected=pytest.approx(94906265.62425156), + msg="sqrt(2^53) ~= 94906265.6", + ), + ExpressionTestCase( + "decimal128_max", + expression={"$sqrt": DECIMAL128_MAX}, + expected=Decimal128("3.162277660168379331998893544432718E+3072"), + msg="sqrt(decimal128 max) ~= 3.16E+3072", + ), + ExpressionTestCase( + "decimal128_small_exponent", + expression={"$sqrt": DECIMAL128_SMALL_EXPONENT}, + expected=Decimal128("3.162277660168379331998893544432719E-3072"), + msg="sqrt(1E-6143) ~= 3.16E-3072", + ), + ExpressionTestCase( + "decimal128_min_positive", + expression={"$sqrt": DECIMAL128_MIN_POSITIVE}, + expected=Decimal128("1E-3088"), + msg="sqrt(1E-6176) = 1E-3088 (exact, even exponent)", + ), + ExpressionTestCase( + "decimal128_large_exponent", + expression={"$sqrt": DECIMAL128_LARGE_EXPONENT}, + expected=Decimal128("1.00000000000000000E+3072"), + msg="sqrt(1E+6144) = 1E+3072", + ), + ExpressionTestCase( + "decimal128_trailing_zero", + expression={"$sqrt": DECIMAL128_TRAILING_ZERO}, + expected=Decimal128("1.0"), + msg="sqrt(decimal 1.0) = 1.0 (trailing-zero scale preserved)", + ), + ExpressionTestCase( + "decimal128_many_trailing_zeros", + expression={"$sqrt": DECIMAL128_MANY_TRAILING_ZEROS}, + expected=Decimal128("1.0000000000000000"), + msg="sqrt(decimal 1.000...) preserves the trailing-zero scale", + ), + ExpressionTestCase( + "double_half", + expression={"$sqrt": DOUBLE_HALF}, + expected=pytest.approx(0.7071067811865476), + msg="sqrt(0.5) ~= 0.70711", + ), + ExpressionTestCase( + "double_one_and_half", + expression={"$sqrt": DOUBLE_ONE_AND_HALF}, + expected=pytest.approx(1.224744871391589), + msg="sqrt(1.5) ~= 1.22474", + ), + ExpressionTestCase( + "double_two_and_half", + expression={"$sqrt": DOUBLE_TWO_AND_HALF}, + expected=pytest.approx(1.5811388300841898), + msg="sqrt(2.5) ~= 1.58114", + ), + ExpressionTestCase( + "double_just_below_half", + expression={"$sqrt": DOUBLE_JUST_BELOW_HALF}, + expected=pytest.approx(0.7071067811865475), + msg="sqrt of double just below 0.5 ~= 0.70711", + ), + ExpressionTestCase( + "double_just_above_half", + expression={"$sqrt": DOUBLE_JUST_ABOVE_HALF}, + expected=pytest.approx(0.7071067816840446), + msg="sqrt of double just above 0.5 ~= 0.70711 (distinct from 0.5)", + ), + ExpressionTestCase( + "decimal_half", + expression={"$sqrt": DECIMAL128_HALF}, + expected=Decimal128("0.7071067811865475244008443621048490"), + msg="sqrt(decimal 0.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_one_and_half", + expression={"$sqrt": DECIMAL128_ONE_AND_HALF}, + expected=Decimal128("1.224744871391589049098642037352946"), + msg="sqrt(decimal 1.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_two_and_half", + expression={"$sqrt": DECIMAL128_TWO_AND_HALF}, + expected=Decimal128("1.581138830084189665999446772216359"), + msg="sqrt(decimal 2.5) to 34-digit precision", + ), + ExpressionTestCase( + "decimal_just_above_half", + expression={"$sqrt": DECIMAL128_JUST_ABOVE_HALF}, + expected=Decimal128("0.7071067811865475244008443621048491"), + msg="34-digit decimal just above 0.5 (last digit differs from the 0.5 result)", + ), + ExpressionTestCase( + "float_infinity", + expression={"$sqrt": FLOAT_INFINITY}, + expected=FLOAT_INFINITY, + msg="sqrt(+Infinity) = +Infinity", + ), + ExpressionTestCase( + "decimal128_infinity", + expression={"$sqrt": DECIMAL128_INFINITY}, + expected=DECIMAL128_INFINITY, + msg="sqrt(decimal +Infinity) = +Infinity", + ), + ExpressionTestCase( + "float_nan", + expression={"$sqrt": FLOAT_NAN}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN for float nan", + ), + ExpressionTestCase( + "decimal128_nan", + expression={"$sqrt": DECIMAL128_NAN}, + expected=DECIMAL128_NAN, + msg="Should return NaN for decimal128 nan", + ), + ExpressionTestCase( + "null_value", + expression={"$sqrt": None}, + expected=None, + msg="Should return null for null value", + ), + ExpressionTestCase( + "missing_value", + expression={"$sqrt": MISSING}, + expected=None, + msg="Should return null for missing value", + ), + ExpressionTestCase( + "single_element_array", + expression={"$sqrt": [25]}, + expected=5.0, + msg="Should accept single-element array as implicit scalar unwrap", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SQRT_PRECISION_LITERAL_TESTS)) +def test_sqrt_precision_literal(collection, test): + """Test $sqrt with literal boundary, decimal128-precision, and special values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +SQRT_PRECISION_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int64_max", + expression={"$sqrt": "$value"}, + doc={"value": INT64_MAX}, + expected=pytest.approx(3.0370004999760455e9), + msg="sqrt(INT64_MAX) ~= 3.037e9", + ), + ExpressionTestCase( + "decimal128_max", + expression={"$sqrt": "$value"}, + doc={"value": DECIMAL128_MAX}, + expected=Decimal128("3.162277660168379331998893544432718E+3072"), + msg="sqrt(decimal128 max) ~= 3.16E+3072", + ), + ExpressionTestCase( + "decimal_half", + expression={"$sqrt": "$value"}, + doc={"value": DECIMAL128_HALF}, + expected=Decimal128("0.7071067811865475244008443621048490"), + msg="sqrt(decimal 0.5) to 34-digit precision", + ), + ExpressionTestCase( + "float_infinity", + expression={"$sqrt": "$value"}, + doc={"value": FLOAT_INFINITY}, + expected=FLOAT_INFINITY, + msg="sqrt(+Infinity) = +Infinity", + ), + ExpressionTestCase( + "float_nan", + expression={"$sqrt": "$value"}, + doc={"value": FLOAT_NAN}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN for float nan", + ), + ExpressionTestCase( + "null_value", + expression={"$sqrt": "$value"}, + doc={"value": None}, + expected=None, + msg="Should return null for null value", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SQRT_PRECISION_INSERT_TESTS)) +def test_sqrt_precision_insert(collection, test): + """Test $sqrt with inserted boundary, decimal128-precision, and special values""" + 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/arithmetic/trunc/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_basic.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_basic.py new file mode 100644 index 000000000..2c0b5be8a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_basic.py @@ -0,0 +1,293 @@ +""" +Basic truncation tests for $trunc expression. + +Covers zero/positive/negative values for all numeric types and +truncate-toward-zero behavior (as opposed to rounding). +""" + +import pytest +from bson import ( + Decimal128, + Int64, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +TRUNC_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_zero", + expression={"$trunc": 0}, + expected=0, + msg="Should truncate int32 zero to itself", + ), + ExpressionTestCase( + "int64_zero", + expression={"$trunc": Int64(0)}, + expected=Int64(0), + msg="Should truncate int64 zero to itself", + ), + ExpressionTestCase( + "double_zero", + expression={"$trunc": 0.0}, + expected=0.0, + msg="Should truncate double zero to itself", + ), + ExpressionTestCase( + "double_negative_zero", + expression={"$trunc": -0.0}, + expected=-0.0, + msg="Should preserve the sign of double negative zero", + ), + ExpressionTestCase( + "decimal_zero", + expression={"$trunc": Decimal128("0")}, + expected=Decimal128("0"), + msg="Should truncate Decimal128 zero to itself", + ), + ExpressionTestCase( + "decimal_negative_zero", + expression={"$trunc": Decimal128("-0")}, + expected=Decimal128("-0"), + msg="Should preserve the sign of Decimal128 negative zero", + ), + ExpressionTestCase( + "int32_positive", + expression={"$trunc": 1}, + expected=1, + msg="Should truncate positive int32 to itself", + ), + ExpressionTestCase( + "int64_positive", + expression={"$trunc": Int64(1)}, + expected=Int64(1), + msg="Should truncate positive int64 to itself", + ), + ExpressionTestCase( + "int32_negative", + expression={"$trunc": -1}, + expected=-1, + msg="Should truncate negative int32 to itself", + ), + ExpressionTestCase( + "int64_negative", + expression={"$trunc": Int64(-1)}, + expected=Int64(-1), + msg="Should truncate negative int64 to itself", + ), + ExpressionTestCase( + "double_positive", + expression={"$trunc": 1.9}, + expected=1.0, + msg="Should truncate positive double toward zero", + ), + ExpressionTestCase( + "double_negative", + expression={"$trunc": -1.9}, + expected=-1.0, + msg="Should truncate negative double toward zero", + ), + ExpressionTestCase( + "double_trunc_down", + expression={"$trunc": 1.4}, + expected=1.0, + msg="Should truncate (not round) 1.4 down to 1.0", + ), + ExpressionTestCase( + "double_trunc_up", + expression={"$trunc": 1.6}, + expected=1.0, + msg="Should truncate (not round) 1.6 down to 1.0", + ), + ExpressionTestCase( + "double_neg_trunc", + expression={"$trunc": -1.4}, + expected=-1.0, + msg="Should truncate (not round) -1.4 toward zero to -1.0", + ), + ExpressionTestCase( + "decimal_positive", + expression={"$trunc": Decimal128("1.9")}, + expected=Decimal128("1"), + msg="Should truncate positive Decimal128 toward zero", + ), + ExpressionTestCase( + "decimal_negative", + expression={"$trunc": Decimal128("-1.9")}, + expected=Decimal128("-1"), + msg="Should truncate negative Decimal128 toward zero", + ), + ExpressionTestCase( + "trunc_positive_large", + expression={"$trunc": 99.99}, + expected=99.0, + msg="Should truncate 99.99 down to 99.0", + ), + ExpressionTestCase( + "trunc_negative_large", + expression={"$trunc": -99.99}, + expected=-99.0, + msg="Should truncate -99.99 toward zero to -99.0", + ), +] + +TRUNC_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_zero", + expression={"$trunc": "$value"}, + doc={"value": 0}, + expected=0, + msg="Should truncate int32 zero to itself", + ), + ExpressionTestCase( + "int64_zero", + expression={"$trunc": "$value"}, + doc={"value": Int64(0)}, + expected=Int64(0), + msg="Should truncate int64 zero to itself", + ), + ExpressionTestCase( + "double_zero", + expression={"$trunc": "$value"}, + doc={"value": 0.0}, + expected=0.0, + msg="Should truncate double zero to itself", + ), + ExpressionTestCase( + "double_negative_zero", + expression={"$trunc": "$value"}, + doc={"value": -0.0}, + expected=-0.0, + msg="Should preserve the sign of double negative zero", + ), + ExpressionTestCase( + "decimal_zero", + expression={"$trunc": "$value"}, + doc={"value": Decimal128("0")}, + expected=Decimal128("0"), + msg="Should truncate Decimal128 zero to itself", + ), + ExpressionTestCase( + "decimal_negative_zero", + expression={"$trunc": "$value"}, + doc={"value": Decimal128("-0")}, + expected=Decimal128("-0"), + msg="Should preserve the sign of Decimal128 negative zero", + ), + ExpressionTestCase( + "int32_positive", + expression={"$trunc": "$value"}, + doc={"value": 1}, + expected=1, + msg="Should truncate positive int32 to itself", + ), + ExpressionTestCase( + "int64_positive", + expression={"$trunc": "$value"}, + doc={"value": Int64(1)}, + expected=Int64(1), + msg="Should truncate positive int64 to itself", + ), + ExpressionTestCase( + "int32_negative", + expression={"$trunc": "$value"}, + doc={"value": -1}, + expected=-1, + msg="Should truncate negative int32 to itself", + ), + ExpressionTestCase( + "int64_negative", + expression={"$trunc": "$value"}, + doc={"value": Int64(-1)}, + expected=Int64(-1), + msg="Should truncate negative int64 to itself", + ), + ExpressionTestCase( + "double_positive", + expression={"$trunc": "$value"}, + doc={"value": 1.9}, + expected=1.0, + msg="Should truncate positive double toward zero", + ), + ExpressionTestCase( + "double_negative", + expression={"$trunc": "$value"}, + doc={"value": -1.9}, + expected=-1.0, + msg="Should truncate negative double toward zero", + ), + ExpressionTestCase( + "double_trunc_down", + expression={"$trunc": "$value"}, + doc={"value": 1.4}, + expected=1.0, + msg="Should truncate (not round) 1.4 down to 1.0", + ), + ExpressionTestCase( + "double_trunc_up", + expression={"$trunc": "$value"}, + doc={"value": 1.6}, + expected=1.0, + msg="Should truncate (not round) 1.6 down to 1.0", + ), + ExpressionTestCase( + "double_neg_trunc", + expression={"$trunc": "$value"}, + doc={"value": -1.4}, + expected=-1.0, + msg="Should truncate (not round) -1.4 toward zero to -1.0", + ), + ExpressionTestCase( + "decimal_positive", + expression={"$trunc": "$value"}, + doc={"value": Decimal128("1.9")}, + expected=Decimal128("1"), + msg="Should truncate positive Decimal128 toward zero", + ), + ExpressionTestCase( + "decimal_negative", + expression={"$trunc": "$value"}, + doc={"value": Decimal128("-1.9")}, + expected=Decimal128("-1"), + msg="Should truncate negative Decimal128 toward zero", + ), + ExpressionTestCase( + "trunc_positive_large", + expression={"$trunc": "$value"}, + doc={"value": 99.99}, + expected=99.0, + msg="Should truncate 99.99 down to 99.0", + ), + ExpressionTestCase( + "trunc_negative_large", + expression={"$trunc": "$value"}, + doc={"value": -99.99}, + expected=-99.0, + msg="Should truncate -99.99 toward zero to -99.0", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TRUNC_LITERAL_TESTS)) +def test_trunc_literal(collection, test): + """Test $trunc with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(TRUNC_INSERT_TESTS)) +def test_trunc_insert(collection, test): + """Test $trunc with inserted document values""" + 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/arithmetic/trunc/test_trunc_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_boundaries.py new file mode 100644 index 000000000..f7492c617 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_boundaries.py @@ -0,0 +1,314 @@ +""" +Boundary tests for $trunc expression. + +Covers INT32/INT64 boundary values, double subnormals (including +negative subnormal), near-min/near-max doubles, max-safe-integer, +and Decimal128 max/min/exponent/trailing-zero boundaries. +""" + +import pytest +from bson import ( + Decimal128, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_LARGE_EXPONENT, + DECIMAL128_MANY_TRAILING_ZEROS, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_SMALL_EXPONENT, + DECIMAL128_TRAILING_ZERO, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_MIN_NEGATIVE_SUBNORMAL, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + INT32_MAX, + INT32_MAX_MINUS_1, + INT32_MIN, + INT32_MIN_PLUS_1, + INT64_MAX, + INT64_MAX_MINUS_1, + INT64_MIN, + INT64_MIN_PLUS_1, +) + +TRUNC_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_max", + expression={"$trunc": INT32_MAX}, + expected=INT32_MAX, + msg="Should preserve INT32_MAX unchanged", + ), + ExpressionTestCase( + "int32_max_minus_1", + expression={"$trunc": INT32_MAX_MINUS_1}, + expected=INT32_MAX_MINUS_1, + msg="Should preserve INT32_MAX - 1 unchanged", + ), + ExpressionTestCase( + "int32_min", + expression={"$trunc": INT32_MIN}, + expected=INT32_MIN, + msg="Should preserve INT32_MIN unchanged", + ), + ExpressionTestCase( + "int32_min_plus_1", + expression={"$trunc": INT32_MIN_PLUS_1}, + expected=INT32_MIN_PLUS_1, + msg="Should preserve INT32_MIN + 1 unchanged", + ), + ExpressionTestCase( + "int64_max", + expression={"$trunc": INT64_MAX}, + expected=INT64_MAX, + msg="Should preserve INT64_MAX unchanged", + ), + ExpressionTestCase( + "int64_max_minus_1", + expression={"$trunc": INT64_MAX_MINUS_1}, + expected=INT64_MAX_MINUS_1, + msg="Should preserve INT64_MAX - 1 unchanged", + ), + ExpressionTestCase( + "int64_min", + expression={"$trunc": INT64_MIN}, + expected=INT64_MIN, + msg="Should preserve INT64_MIN unchanged", + ), + ExpressionTestCase( + "int64_min_plus_1", + expression={"$trunc": INT64_MIN_PLUS_1}, + expected=INT64_MIN_PLUS_1, + msg="Should preserve INT64_MIN + 1 unchanged", + ), + ExpressionTestCase( + "double_min_subnormal", + expression={"$trunc": DOUBLE_MIN_SUBNORMAL}, + expected=0.0, + msg="Should truncate the smallest positive subnormal double to 0.0", + ), + ExpressionTestCase( + "double_min_negative_subnormal", + expression={"$trunc": DOUBLE_MIN_NEGATIVE_SUBNORMAL}, + expected=-0.0, + msg="Should truncate the smallest negative subnormal double to -0.0", + ), + ExpressionTestCase( + "double_near_min", + expression={"$trunc": DOUBLE_NEAR_MIN}, + expected=0.0, + msg="Should truncate a double near the minimum positive value to 0.0", + ), + ExpressionTestCase( + "double_near_max", + expression={"$trunc": DOUBLE_NEAR_MAX}, + expected=DOUBLE_NEAR_MAX, + msg="Should preserve a double near the maximum value unchanged", + ), + ExpressionTestCase( + "double_max_safe_integer", + expression={"$trunc": float(DOUBLE_MAX_SAFE_INTEGER)}, + expected=float(DOUBLE_MAX_SAFE_INTEGER), + msg="Should preserve the double max safe integer unchanged", + ), + ExpressionTestCase( + "decimal128_max", + expression={"$trunc": DECIMAL128_MAX}, + expected=DECIMAL128_MAX, + msg="Should preserve Decimal128 max value unchanged", + ), + ExpressionTestCase( + "decimal128_min", + expression={"$trunc": DECIMAL128_MIN}, + expected=DECIMAL128_MIN, + msg="Should preserve Decimal128 min value unchanged", + ), + ExpressionTestCase( + "decimal128_small_exponent", + expression={"$trunc": DECIMAL128_SMALL_EXPONENT}, + expected=Decimal128("0"), + msg="Should truncate a Decimal128 with a very small exponent to 0", + ), + ExpressionTestCase( + "decimal128_large_exponent", + expression={"$trunc": DECIMAL128_LARGE_EXPONENT}, + expected=DECIMAL128_LARGE_EXPONENT, + msg="Should preserve a Decimal128 with a very large exponent unchanged", + ), + ExpressionTestCase( + "decimal128_trailing_zero", + expression={"$trunc": DECIMAL128_TRAILING_ZERO}, + expected=Decimal128("1"), + msg="Should truncate a Decimal128 with a trailing-zero fraction to its integer part", + ), + ExpressionTestCase( + "decimal128_many_trailing_zeros", + expression={"$trunc": DECIMAL128_MANY_TRAILING_ZEROS}, + expected=Decimal128("1"), + msg="Should truncate a Decimal128 with many trailing zeros to its integer part", + ), +] + +TRUNC_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_max", + expression={"$trunc": "$value"}, + doc={"value": INT32_MAX}, + expected=INT32_MAX, + msg="Should preserve INT32_MAX unchanged", + ), + ExpressionTestCase( + "int32_max_minus_1", + expression={"$trunc": "$value"}, + doc={"value": INT32_MAX_MINUS_1}, + expected=INT32_MAX_MINUS_1, + msg="Should preserve INT32_MAX - 1 unchanged", + ), + ExpressionTestCase( + "int32_min", + expression={"$trunc": "$value"}, + doc={"value": INT32_MIN}, + expected=INT32_MIN, + msg="Should preserve INT32_MIN unchanged", + ), + ExpressionTestCase( + "int32_min_plus_1", + expression={"$trunc": "$value"}, + doc={"value": INT32_MIN_PLUS_1}, + expected=INT32_MIN_PLUS_1, + msg="Should preserve INT32_MIN + 1 unchanged", + ), + ExpressionTestCase( + "int64_max", + expression={"$trunc": "$value"}, + doc={"value": INT64_MAX}, + expected=INT64_MAX, + msg="Should preserve INT64_MAX unchanged", + ), + ExpressionTestCase( + "int64_max_minus_1", + expression={"$trunc": "$value"}, + doc={"value": INT64_MAX_MINUS_1}, + expected=INT64_MAX_MINUS_1, + msg="Should preserve INT64_MAX - 1 unchanged", + ), + ExpressionTestCase( + "int64_min", + expression={"$trunc": "$value"}, + doc={"value": INT64_MIN}, + expected=INT64_MIN, + msg="Should preserve INT64_MIN unchanged", + ), + ExpressionTestCase( + "int64_min_plus_1", + expression={"$trunc": "$value"}, + doc={"value": INT64_MIN_PLUS_1}, + expected=INT64_MIN_PLUS_1, + msg="Should preserve INT64_MIN + 1 unchanged", + ), + ExpressionTestCase( + "double_min_subnormal", + expression={"$trunc": "$value"}, + doc={"value": DOUBLE_MIN_SUBNORMAL}, + expected=0.0, + msg="Should truncate the smallest positive subnormal double to 0.0", + ), + ExpressionTestCase( + "double_min_negative_subnormal", + expression={"$trunc": "$value"}, + doc={"value": DOUBLE_MIN_NEGATIVE_SUBNORMAL}, + expected=-0.0, + msg="Should truncate the smallest negative subnormal double to -0.0", + ), + ExpressionTestCase( + "double_near_min", + expression={"$trunc": "$value"}, + doc={"value": DOUBLE_NEAR_MIN}, + expected=0.0, + msg="Should truncate a double near the minimum positive value to 0.0", + ), + ExpressionTestCase( + "double_near_max", + expression={"$trunc": "$value"}, + doc={"value": DOUBLE_NEAR_MAX}, + expected=DOUBLE_NEAR_MAX, + msg="Should preserve a double near the maximum value unchanged", + ), + ExpressionTestCase( + "double_max_safe_integer", + expression={"$trunc": "$value"}, + doc={"value": float(DOUBLE_MAX_SAFE_INTEGER)}, + expected=float(DOUBLE_MAX_SAFE_INTEGER), + msg="Should preserve the double max safe integer unchanged", + ), + ExpressionTestCase( + "decimal128_max", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_MAX}, + expected=DECIMAL128_MAX, + msg="Should preserve Decimal128 max value unchanged", + ), + ExpressionTestCase( + "decimal128_min", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_MIN}, + expected=DECIMAL128_MIN, + msg="Should preserve Decimal128 min value unchanged", + ), + ExpressionTestCase( + "decimal128_small_exponent", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_SMALL_EXPONENT}, + expected=Decimal128("0"), + msg="Should truncate a Decimal128 with a very small exponent to 0", + ), + ExpressionTestCase( + "decimal128_large_exponent", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_LARGE_EXPONENT}, + expected=DECIMAL128_LARGE_EXPONENT, + msg="Should preserve a Decimal128 with a very large exponent unchanged", + ), + ExpressionTestCase( + "decimal128_trailing_zero", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_TRAILING_ZERO}, + expected=Decimal128("1"), + msg="Should truncate a Decimal128 with a trailing-zero fraction to its integer part", + ), + ExpressionTestCase( + "decimal128_many_trailing_zeros", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_MANY_TRAILING_ZEROS}, + expected=Decimal128("1"), + msg="Should truncate a Decimal128 with many trailing zeros to its integer part", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TRUNC_LITERAL_TESTS)) +def test_trunc_literal(collection, test): + """Test $trunc with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(TRUNC_INSERT_TESTS)) +def test_trunc_insert(collection, test): + """Test $trunc with inserted document values""" + 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/arithmetic/trunc/test_trunc_bson_type_validation.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_bson_type_validation.py new file mode 100644 index 000000000..529d55f45 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_bson_type_validation.py @@ -0,0 +1,63 @@ +""" +BSON type validation tests for $trunc expression. + +Systematically verifies that the number (first) input accepts the four numeric +types plus null, and rejects every other BSON type. The `place`-position +validation lives in test_trunc_errors.py. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.assertions import assertNotError +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import NON_NUMERIC_TYPE_ERROR + +TRUNC_BSON_PARAMS = [ + BsonTypeTestCase( + id="number", + msg="$trunc number should reject non-numeric types", + keyword="number", + valid_types=[ + BsonType.DOUBLE, + BsonType.INT, + BsonType.LONG, + BsonType.DECIMAL, + BsonType.NULL, + ], + default_error_code=NON_NUMERIC_TYPE_ERROR, + ), +] + +REJECTION_CASES = generate_bson_rejection_test_cases(TRUNC_BSON_PARAMS) +ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(TRUNC_BSON_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", REJECTION_CASES) +def test_trunc_bson_type_rejected(collection, bson_type, sample_value, spec): + """Verifies $trunc rejects invalid BSON types for the number input.""" + result = execute_expression_with_insert( + collection, {"$trunc": "$value"}, {"value": sample_value} + ) + assert_expression_result( + result, + error_code=spec.expected_code(bson_type), + msg=f"{spec.msg}: {bson_type.value}", + ) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) +def test_trunc_bson_type_accepted(collection, bson_type, sample_value, spec): + """Verifies $trunc accepts valid numeric BSON types (and null).""" + result = execute_expression_with_insert( + collection, {"$trunc": "$value"}, {"value": sample_value} + ) + assertNotError(result, msg=f"$trunc number should accept {bson_type.value}") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_errors.py new file mode 100644 index 000000000..32ec449f4 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_errors.py @@ -0,0 +1,297 @@ +""" +Error tests for $trunc expression. + +Covers non-numeric type rejection, invalid/out-of-bounds place values, +place type errors (bool, object, array, infinity, fractional decimal128), +arity errors, and composite/array-index field path rejection. +""" + +import pytest +from bson import ( + Decimal128, + Int64, +) +from bson.binary import Binary + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + EXPRESSION_ARITY_ERROR, + INVALID_POSITION_ERROR, + INVALID_TYPE_ERROR, + NON_INTEGRAL_POSITION_ERROR, + NON_NUMERIC_TYPE_ERROR, + OUT_OF_RANGE_CONVERSION_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_NAN, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +TRUNC_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty_object", + expression={"$trunc": {}}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Should reject non-numeric object input", + ), + ExpressionTestCase( + "invalid_positive_place_101", + expression={"$trunc": [123456789012345.123, 101]}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject place value 101 (out of bounds)", + ), + ExpressionTestCase( + "invalid_negative_place_21", + expression={"$trunc": [123456789012345.123, -21]}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject place value -21 (out of bounds)", + ), + ExpressionTestCase( + "place_non_integral", + expression={"$trunc": [1, 10.5]}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject non-integral double place", + ), + ExpressionTestCase( + "place_nan", + expression={"$trunc": [1, FLOAT_NAN]}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject NaN place", + ), + ExpressionTestCase( + "place_decimal_nan", + expression={"$trunc": [1, DECIMAL128_NAN]}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject Decimal128 NaN place", + ), + ExpressionTestCase( + "place_string", + expression={"$trunc": [1, "2"]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject string place", + ), + ExpressionTestCase( + "place_binary", + expression={"$trunc": [1, Binary(b"", 0)]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject binary place", + ), + ExpressionTestCase( + "place_int64_out_of_bounds", + expression={"$trunc": [1, Int64(101)]}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject Int64 place out of bounds", + ), + ExpressionTestCase( + "place_decimal_out_of_bounds", + expression={"$trunc": [1, Decimal128("-21")]}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject Decimal128 place out of bounds", + ), + ExpressionTestCase( + "three_args", + expression={"$trunc": [1, 2, 3]}, + error_code=EXPRESSION_ARITY_ERROR, + msg="Should reject 3-argument arity", + ), + ExpressionTestCase( + "empty_array", + expression={"$trunc": []}, + error_code=EXPRESSION_ARITY_ERROR, + msg="Should reject empty array arity", + ), + ExpressionTestCase( + "place_infinity", + expression={"$trunc": [1.5, FLOAT_INFINITY]}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject infinity place", + ), + ExpressionTestCase( + "place_negative_infinity", + expression={"$trunc": [1.5, FLOAT_NEGATIVE_INFINITY]}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject negative infinity place", + ), + ExpressionTestCase( + "place_bool", + expression={"$trunc": [1.5, True]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject boolean place", + ), + ExpressionTestCase( + "place_object", + expression={"$trunc": [1.5, {"a": 1}]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject object place", + ), + ExpressionTestCase( + "place_array_value", + expression={"$trunc": [1.5, [2]]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject array place", + ), + ExpressionTestCase( + "place_decimal_fractional", + expression={"$trunc": [1.5, Decimal128("0.5")]}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject fractional decimal128 place", + ), +] + + +TRUNC_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty_object", + expression={"$trunc": "$value"}, + doc={"value": {}}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Should reject non-numeric object input", + ), + ExpressionTestCase( + "invalid_positive_place_101", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 123456789012345.123, "place": 101}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject place value 101 (out of bounds)", + ), + ExpressionTestCase( + "invalid_negative_place_21", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 123456789012345.123, "place": -21}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject place value -21 (out of bounds)", + ), + ExpressionTestCase( + "place_non_integral", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1, "place": 10.5}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject non-integral double place", + ), + ExpressionTestCase( + "place_nan", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1, "place": FLOAT_NAN}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject NaN place", + ), + ExpressionTestCase( + "place_decimal_nan", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1, "place": DECIMAL128_NAN}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject Decimal128 NaN place", + ), + ExpressionTestCase( + "place_string", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1, "place": "2"}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject string place", + ), + ExpressionTestCase( + "place_binary", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1, "place": Binary(b"", 0)}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject binary place", + ), + ExpressionTestCase( + "place_int64_out_of_bounds", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1, "place": Int64(101)}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject Int64 place out of bounds", + ), + ExpressionTestCase( + "place_decimal_out_of_bounds", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1, "place": Decimal128("-21")}, + error_code=INVALID_POSITION_ERROR, + msg="Should reject Decimal128 place out of bounds", + ), + ExpressionTestCase( + "empty_array_value", + expression={"$trunc": "$value"}, + doc={"value": []}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Should reject non-numeric empty array input", + ), + ExpressionTestCase( + "place_infinity", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.5, "place": FLOAT_INFINITY}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject infinity place", + ), + ExpressionTestCase( + "place_negative_infinity", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.5, "place": FLOAT_NEGATIVE_INFINITY}, + error_code=OUT_OF_RANGE_CONVERSION_ERROR, + msg="Should reject negative infinity place", + ), + ExpressionTestCase( + "place_bool", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.5, "place": True}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject boolean place", + ), + ExpressionTestCase( + "place_object", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.5, "place": {"a": 1}}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject object place", + ), + ExpressionTestCase( + "place_array_value", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.5, "place": [2]}, + error_code=INVALID_TYPE_ERROR, + msg="Should reject array place", + ), + ExpressionTestCase( + "place_decimal_fractional", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.5, "place": Decimal128("0.5")}, + error_code=NON_INTEGRAL_POSITION_ERROR, + msg="Should reject fractional decimal128 place", + ), + ExpressionTestCase( + "composite_array_field", + expression={"$trunc": "$x.y"}, + doc={"x": [{"y": 9.876}, {"y": 2}]}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Should reject composite array from $x.y on array-of-objects", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TRUNC_LITERAL_TESTS)) +def test_trunc_literal(collection, test): + """Test $trunc with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(TRUNC_INSERT_TESTS)) +def test_trunc_insert(collection, test): + """Test $trunc with inserted document values""" + 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/arithmetic/trunc/test_trunc_places.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_places.py new file mode 100644 index 000000000..ae8f50f97 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_places.py @@ -0,0 +1,700 @@ +""" +Place argument, nested-expression, and field-lookup tests for $trunc expression. + +Covers positive/negative place values, place types (int64, decimal128, +whole double), decimal128 zero-padding, single-element array form, +null/missing short-circuiting, NaN/Infinity with place, nested $trunc, +and field-path resolution. +""" + +import math + +import pytest +from bson import ( + Decimal128, + Int64, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + MISSING, +) + +TRUNC_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_place_0", + expression={"$trunc": [1.12345678901234, 0]}, + expected=1.0, + msg="Should truncate a double to 0 decimal places", + ), + ExpressionTestCase( + "double_place_2", + expression={"$trunc": [1.12345678901234, 2]}, + expected=1.12, + msg="Should truncate a double to 2 decimal places", + ), + ExpressionTestCase( + "double_place_10", + expression={"$trunc": [1.12345678901234, 10]}, + expected=1.1234567890, + msg="Should truncate a double to 10 decimal places", + ), + ExpressionTestCase( + "double_place_13", + expression={"$trunc": [1.12345678901234, 13]}, + expected=1.1234567890123, + msg="Should truncate a double to 13 decimal places", + ), + ExpressionTestCase( + "double_place_14", + expression={"$trunc": [1.12345678901234, 14]}, + expected=pytest.approx(1.12345678901234), + msg="Should preserve full double precision at place 14", + ), + ExpressionTestCase( + "double_place_20", + expression={"$trunc": [1.12345678901234, 20]}, + expected=pytest.approx(1.12345678901234), + msg="Should preserve full double precision when place exceeds available digits", + ), + ExpressionTestCase( + "double_ieee754_divergence", + expression={"$trunc": [0.29, 2]}, + expected=0.28, + msg=( + "IEEE-754 double 0.29 is stored as ~0.28999999999999998, so truncating to 2 places " + "drops below its literal spelling to 0.28" + ), + ), + ExpressionTestCase( + "double_ieee754_divergence_2", + expression={"$trunc": [1.005, 2]}, + expected=1.00, + msg=( + "IEEE-754 double 1.005 is stored as ~1.00499999999999989, so truncating to 2 places " + "drops to 1.00 instead of 1.01" + ), + ), + ExpressionTestCase( + "decimal128_no_ieee754_divergence", + expression={"$trunc": [Decimal128("0.29"), 2]}, + expected=Decimal128("0.29"), + msg=( + "Decimal128 has exact base-10 representation, so truncating 0.29 to 2 places " + "stays 0.29 (contrast with the double_ieee754_divergence case)" + ), + ), + ExpressionTestCase( + "place_0", + expression={"$trunc": [Decimal128("1.123456789012345678901234567890123"), 0]}, + expected=Decimal128("1"), + msg="Should truncate a high-precision Decimal128 to 0 decimal places", + ), + ExpressionTestCase( + "place_2", + expression={"$trunc": [Decimal128("1.123456789012345678901234567890123"), 2]}, + expected=Decimal128("1.12"), + msg="Should truncate a high-precision Decimal128 to 2 decimal places", + ), + ExpressionTestCase( + "place_10", + expression={"$trunc": [Decimal128("1.123456789012345678901234567890123"), 10]}, + expected=Decimal128("1.1234567890"), + msg="Should truncate a high-precision Decimal128 to 10 decimal places", + ), + ExpressionTestCase( + "place_32", + expression={"$trunc": [Decimal128("1.123456789012345678901234567890123"), 32]}, + expected=Decimal128("1.12345678901234567890123456789012"), + msg="Should truncate a Decimal128 to 32 decimal places", + ), + ExpressionTestCase( + "place_33", + expression={"$trunc": [Decimal128("1.123456789012345678901234567890123"), 33]}, + expected=Decimal128("1.123456789012345678901234567890123"), + msg="Should preserve a Decimal128 unchanged at its natural 33-digit precision", + ), + ExpressionTestCase( + "place_34", + expression={"$trunc": [Decimal128("1.123456789012345678901234567890123"), 34]}, + expected=Decimal128("1.1234567890123456789012345678901230"), + msg="Should zero-pad a Decimal128 to 34 decimal places", + ), + ExpressionTestCase( + "place_99", + expression={"$trunc": [Decimal128("1.123456789012345678901234567890123"), 99]}, + expected=Decimal128("1.1234567890123456789012345678901230"), + msg="Should cap Decimal128 truncation at 34 decimal places for place 99", + ), + ExpressionTestCase( + "place_100", + expression={"$trunc": [Decimal128("1.123456789012345678901234567890123"), 100]}, + expected=Decimal128("1.1234567890123456789012345678901230"), + msg="Should cap Decimal128 truncation at 34 decimal places for place 100", + ), + ExpressionTestCase( + "zero_pad_place_2", + expression={"$trunc": [Decimal128("1.1"), 2]}, + expected=Decimal128("1.10"), + msg="Should zero-pad Decimal128(1.1) to 2 decimal places", + ), + ExpressionTestCase( + "zero_pad_place_10", + expression={"$trunc": [Decimal128("1.1"), 10]}, + expected=Decimal128("1.1000000000"), + msg="Should zero-pad Decimal128(1.1) to 10 decimal places", + ), + ExpressionTestCase( + "zero_pad_place_33", + expression={"$trunc": [Decimal128("1.1"), 33]}, + expected=Decimal128("1.100000000000000000000000000000000"), + msg="Should zero-pad Decimal128(1.1) to 33 decimal places", + ), + ExpressionTestCase( + "place_int64", + expression={"$trunc": [1.12345, Int64(2)]}, + expected=1.12, + msg="Should accept an Int64 place argument", + ), + ExpressionTestCase( + "place_decimal", + expression={"$trunc": [1.12345, Decimal128("2")]}, + expected=1.12, + msg="Should accept a Decimal128 place argument", + ), + ExpressionTestCase( + "place_double_whole", + expression={"$trunc": [1.2345, 2.0]}, + expected=1.23, + msg="Should accept a whole-number double as the place argument", + ), + ExpressionTestCase( + "single_element_array", + expression={"$trunc": [1.9]}, + expected=1.0, + msg="Should default place to 0 for single-element array form", + ), + ExpressionTestCase( + "int32_with_place", + expression={"$trunc": [5, 3]}, + expected=5, + msg="Should leave an int32 with no fractional part unchanged when given a place", + ), + ExpressionTestCase( + "int64_with_place", + expression={"$trunc": [Int64(5), 3]}, + expected=Int64(5), + msg="Should leave an int64 with no fractional part unchanged when given a place", + ), + ExpressionTestCase( + "neg_place_-1", + expression={"$trunc": [123456789012345.123, -1]}, + expected=123456789012340.0, + msg="Should truncate to the tens place with place -1", + ), + ExpressionTestCase( + "neg_place_-2", + expression={"$trunc": [123456789012345.123, -2]}, + expected=123456789012300.0, + msg="Should truncate to the hundreds place with place -2", + ), + ExpressionTestCase( + "neg_place_-10", + expression={"$trunc": [123456789012345.123, -10]}, + expected=123450000000000.0, + msg="Should truncate to the ten-billions place with place -10", + ), + ExpressionTestCase( + "neg_place_-14", + expression={"$trunc": [123456789012345.123, -14]}, + expected=100000000000000.0, + msg="Should truncate to the highest digit with place -14", + ), + ExpressionTestCase( + "neg_place_-15", + expression={"$trunc": [123456789012345.123, -15]}, + expected=0.0, + msg="Should truncate to 0.0 when place exceeds the value's magnitude", + ), + ExpressionTestCase( + "neg_place_-19", + expression={"$trunc": [123456789012345.123, -19]}, + expected=0.0, + msg="Should truncate to 0.0 for a large negative place", + ), + ExpressionTestCase( + "neg_place_-20", + expression={"$trunc": [123456789012345.123, -20]}, + expected=0.0, + msg="Should truncate to 0.0 at the minimum allowed negative place", + ), + ExpressionTestCase( + "neg_place_int64", + expression={"$trunc": [Int64(1234), Int64(-2)]}, + expected=Int64(1200), + msg="Should truncate an Int64 value using a negative Int64 place", + ), + ExpressionTestCase( + "neg_place_decimal", + expression={"$trunc": [Decimal128("23.298"), Decimal128("-1")]}, + expected=Decimal128("2E+1"), + msg="Should truncate a Decimal128 value using a negative Decimal128 place", + ), + ExpressionTestCase( + "place_null", + expression={"$trunc": [5.7, None]}, + expected=None, + msg="Should return null when place is null", + ), + ExpressionTestCase( + "nan_with_place_0", + expression={"$trunc": [FLOAT_NAN, 0]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN for NaN with place", + ), + ExpressionTestCase( + "infinity_with_place_0", + expression={"$trunc": [FLOAT_INFINITY, 0]}, + expected=FLOAT_INFINITY, + msg="Should return Infinity for Infinity with place", + ), + ExpressionTestCase( + "neg_infinity_with_place_0", + expression={"$trunc": [FLOAT_NEGATIVE_INFINITY, 0]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="Should return -Infinity for -Infinity with place", + ), + ExpressionTestCase( + "decimal_nan_with_place_0", + expression={"$trunc": [DECIMAL128_NAN, 0]}, + expected=DECIMAL128_NAN, + msg="Should return decimal NaN for decimal NaN with place", + ), + ExpressionTestCase( + "decimal_infinity_with_place_0", + expression={"$trunc": [DECIMAL128_INFINITY, 0]}, + expected=DECIMAL128_INFINITY, + msg="Should return decimal Infinity for decimal Infinity with place", + ), + ExpressionTestCase( + "neg_value_neg_place_-1", + expression={"$trunc": [-1234.5678, -1]}, + expected=-1230.0, + msg="Should truncate negative value with negative place -1", + ), + ExpressionTestCase( + "neg_value_neg_place_-20", + expression={"$trunc": [-1234.5678, -20]}, + expected=-0.0, + msg="Should truncate negative value with negative place -20", + ), + ExpressionTestCase( + "missing_value", + expression={"$trunc": MISSING}, + expected=None, + msg="Should return null for a missing field value", + ), +] + + +TRUNC_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_place_0", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.12345678901234, "place": 0}, + expected=1.0, + msg="Should truncate a double to 0 decimal places", + ), + ExpressionTestCase( + "double_place_2", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.12345678901234, "place": 2}, + expected=1.12, + msg="Should truncate a double to 2 decimal places", + ), + ExpressionTestCase( + "double_place_10", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.12345678901234, "place": 10}, + expected=1.1234567890, + msg="Should truncate a double to 10 decimal places", + ), + ExpressionTestCase( + "double_place_13", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.12345678901234, "place": 13}, + expected=1.1234567890123, + msg="Should truncate a double to 13 decimal places", + ), + ExpressionTestCase( + "double_place_14", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.12345678901234, "place": 14}, + expected=pytest.approx(1.12345678901234), + msg="Should preserve full double precision at place 14", + ), + ExpressionTestCase( + "double_place_20", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.12345678901234, "place": 20}, + expected=pytest.approx(1.12345678901234), + msg="Should preserve full double precision when place exceeds available digits", + ), + ExpressionTestCase( + "double_ieee754_divergence", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 0.29, "place": 2}, + expected=0.28, + msg=( + "IEEE-754 double 0.29 is stored as ~0.28999999999999998, so truncating to 2 places " + "drops below its literal spelling to 0.28" + ), + ), + ExpressionTestCase( + "double_ieee754_divergence_2", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.005, "place": 2}, + expected=1.00, + msg=( + "IEEE-754 double 1.005 is stored as ~1.00499999999999989, so truncating to 2 places " + "drops to 1.00 instead of 1.01" + ), + ), + ExpressionTestCase( + "decimal128_no_ieee754_divergence", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("0.29"), "place": 2}, + expected=Decimal128("0.29"), + msg=( + "Decimal128 has exact base-10 representation, so truncating 0.29 to 2 places " + "stays 0.29 (contrast with the double_ieee754_divergence case)" + ), + ), + ExpressionTestCase( + "place_0", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("1.123456789012345678901234567890123"), "place": 0}, + expected=Decimal128("1"), + msg="Should truncate a high-precision Decimal128 to 0 decimal places", + ), + ExpressionTestCase( + "place_2", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("1.123456789012345678901234567890123"), "place": 2}, + expected=Decimal128("1.12"), + msg="Should truncate a high-precision Decimal128 to 2 decimal places", + ), + ExpressionTestCase( + "place_10", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("1.123456789012345678901234567890123"), "place": 10}, + expected=Decimal128("1.1234567890"), + msg="Should truncate a high-precision Decimal128 to 10 decimal places", + ), + ExpressionTestCase( + "place_32", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("1.123456789012345678901234567890123"), "place": 32}, + expected=Decimal128("1.12345678901234567890123456789012"), + msg="Should truncate a Decimal128 to 32 decimal places", + ), + ExpressionTestCase( + "place_33", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("1.123456789012345678901234567890123"), "place": 33}, + expected=Decimal128("1.123456789012345678901234567890123"), + msg="Should preserve a Decimal128 unchanged at its natural 33-digit precision", + ), + ExpressionTestCase( + "place_34", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("1.123456789012345678901234567890123"), "place": 34}, + expected=Decimal128("1.1234567890123456789012345678901230"), + msg="Should zero-pad a Decimal128 to 34 decimal places", + ), + ExpressionTestCase( + "place_99", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("1.123456789012345678901234567890123"), "place": 99}, + expected=Decimal128("1.1234567890123456789012345678901230"), + msg="Should cap Decimal128 truncation at 34 decimal places for place 99", + ), + ExpressionTestCase( + "place_100", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("1.123456789012345678901234567890123"), "place": 100}, + expected=Decimal128("1.1234567890123456789012345678901230"), + msg="Should cap Decimal128 truncation at 34 decimal places for place 100", + ), + ExpressionTestCase( + "zero_pad_place_2", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("1.1"), "place": 2}, + expected=Decimal128("1.10"), + msg="Should zero-pad Decimal128(1.1) to 2 decimal places", + ), + ExpressionTestCase( + "zero_pad_place_10", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("1.1"), "place": 10}, + expected=Decimal128("1.1000000000"), + msg="Should zero-pad Decimal128(1.1) to 10 decimal places", + ), + ExpressionTestCase( + "zero_pad_place_33", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("1.1"), "place": 33}, + expected=Decimal128("1.100000000000000000000000000000000"), + msg="Should zero-pad Decimal128(1.1) to 33 decimal places", + ), + ExpressionTestCase( + "place_int64", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.12345, "place": Int64(2)}, + expected=1.12, + msg="Should accept an Int64 place argument", + ), + ExpressionTestCase( + "place_decimal", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.12345, "place": Decimal128("2")}, + expected=1.12, + msg="Should accept a Decimal128 place argument", + ), + ExpressionTestCase( + "place_double_whole", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 1.2345, "place": 2.0}, + expected=1.23, + msg="Should accept a whole-number double as the place argument", + ), + ExpressionTestCase( + "int32_with_place", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 5, "place": 3}, + expected=5, + msg="Should leave an int32 with no fractional part unchanged when given a place", + ), + ExpressionTestCase( + "int64_with_place", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Int64(5), "place": 3}, + expected=Int64(5), + msg="Should leave an int64 with no fractional part unchanged when given a place", + ), + ExpressionTestCase( + "neg_place_-1", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 123456789012345.123, "place": -1}, + expected=123456789012340.0, + msg="Should truncate to the tens place with place -1", + ), + ExpressionTestCase( + "neg_place_-2", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 123456789012345.123, "place": -2}, + expected=123456789012300.0, + msg="Should truncate to the hundreds place with place -2", + ), + ExpressionTestCase( + "neg_place_-10", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 123456789012345.123, "place": -10}, + expected=123450000000000.0, + msg="Should truncate to the ten-billions place with place -10", + ), + ExpressionTestCase( + "neg_place_-14", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 123456789012345.123, "place": -14}, + expected=100000000000000.0, + msg="Should truncate to the highest digit with place -14", + ), + ExpressionTestCase( + "neg_place_-15", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 123456789012345.123, "place": -15}, + expected=0.0, + msg="Should truncate to 0.0 when place exceeds the value's magnitude", + ), + ExpressionTestCase( + "neg_place_-19", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 123456789012345.123, "place": -19}, + expected=0.0, + msg="Should truncate to 0.0 for a large negative place", + ), + ExpressionTestCase( + "neg_place_-20", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 123456789012345.123, "place": -20}, + expected=0.0, + msg="Should truncate to 0.0 at the minimum allowed negative place", + ), + ExpressionTestCase( + "neg_place_int64", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Int64(1234), "place": Int64(-2)}, + expected=Int64(1200), + msg="Should truncate an Int64 value using a negative Int64 place", + ), + ExpressionTestCase( + "neg_place_decimal", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": Decimal128("23.298"), "place": Decimal128("-1")}, + expected=Decimal128("2E+1"), + msg="Should truncate a Decimal128 value using a negative Decimal128 place", + ), + ExpressionTestCase( + "place_null", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": 5.7, "place": None}, + expected=None, + msg="Should return null when place is null", + ), + ExpressionTestCase( + "nonexistent_place_field", + expression={"$trunc": ["$value", "$nonexistent"]}, + doc={"value": 5.678}, + expected=None, + msg="Should return null when place field does not exist", + ), + ExpressionTestCase( + "nan_with_place_0", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": FLOAT_NAN, "place": 0}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should return NaN for NaN with place", + ), + ExpressionTestCase( + "infinity_with_place_0", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": FLOAT_INFINITY, "place": 0}, + expected=FLOAT_INFINITY, + msg="Should return Infinity for Infinity with place", + ), + ExpressionTestCase( + "neg_infinity_with_place_0", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": FLOAT_NEGATIVE_INFINITY, "place": 0}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="Should return -Infinity for -Infinity with place", + ), + ExpressionTestCase( + "decimal_nan_with_place_0", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": DECIMAL128_NAN, "place": 0}, + expected=DECIMAL128_NAN, + msg="Should return decimal NaN for decimal NaN with place", + ), + ExpressionTestCase( + "decimal_infinity_with_place_0", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": DECIMAL128_INFINITY, "place": 0}, + expected=DECIMAL128_INFINITY, + msg="Should return decimal Infinity for decimal Infinity with place", + ), + ExpressionTestCase( + "neg_value_neg_place_-1", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": -1234.5678, "place": -1}, + expected=-1230.0, + msg="Should truncate negative value with negative place -1", + ), + ExpressionTestCase( + "neg_value_neg_place_-20", + expression={"$trunc": ["$value", "$place"]}, + doc={"value": -1234.5678, "place": -20}, + expected=-0.0, + msg="Should truncate negative value with negative place -20", + ), +] + + +TRUNC_NESTED_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_trunc_2", + expression={"$trunc": {"$trunc": 4.6}}, + expected=4.0, + msg="Should evaluate nested $trunc", + ), + ExpressionTestCase( + "nested_trunc_2_large", + expression={"$trunc": {"$trunc": 9.99}}, + expected=9.0, + msg="Should evaluate nested $trunc with larger input", + ), + ExpressionTestCase( + "nested_trunc_3_with_place", + expression={"$trunc": [{"$trunc": [{"$trunc": [9.876, 2]}, 1]}]}, + expected=9.0, + msg="Should evaluate triple-nested $trunc with place", + ), +] + + +TRUNC_FIELD_LOOKUP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_field", + expression={"$trunc": "$a.b"}, + doc={"a": {"b": 4.6}}, + expected=4.0, + msg="Should resolve nested field path", + ), + ExpressionTestCase( + "nonexistent_field", + expression={"$trunc": "$a.nonexistent"}, + doc={"a": {"missing": 1}}, + expected=None, + msg="Should return null for nonexistent field path", + ), + ExpressionTestCase( + "array_index", + expression={"$trunc": {"$arrayElemAt": ["$arr", 0]}}, + doc={"arr": [4.6, 3.2, 1.8]}, + expected=4.0, + msg="Should resolve value from array index", + ), + ExpressionTestCase( + "deeply_nested_field", + expression={"$trunc": "$a.b.c.d"}, + doc={"a": {"b": {"c": {"d": 5.9}}}}, + expected=5.0, + msg="Should resolve deeply nested field path", + ), +] + + +TRUNC_LITERAL_ALL_TESTS: list[ExpressionTestCase] = TRUNC_LITERAL_TESTS + TRUNC_NESTED_TESTS +TRUNC_INSERT_ALL_TESTS: list[ExpressionTestCase] = TRUNC_INSERT_TESTS + TRUNC_FIELD_LOOKUP_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TRUNC_LITERAL_ALL_TESTS)) +def test_trunc_literal(collection, test): + """Test $trunc with literal values and nested expressions.""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(TRUNC_INSERT_ALL_TESTS)) +def test_trunc_insert(collection, test): + """Test $trunc with inserted document values and field-path lookups.""" + 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/arithmetic/trunc/test_trunc_special_values.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_special_values.py new file mode 100644 index 000000000..541378dd9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_special_values.py @@ -0,0 +1,342 @@ +""" +Half-value and special value tests for $trunc expression. + +Covers half-boundary precision (truncation is not rounding) for double +and Decimal128, Infinity, NaN, and null. +""" + +import math + +import pytest +from bson import ( + Decimal128, +) + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_HALF, + DECIMAL128_INFINITY, + DECIMAL128_JUST_ABOVE_HALF, + DECIMAL128_JUST_BELOW_HALF, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_HALF, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ONE_AND_HALF, + DECIMAL128_ONE_AND_HALF, + DECIMAL128_TWO_AND_HALF, + DOUBLE_HALF, + DOUBLE_JUST_ABOVE_HALF, + DOUBLE_JUST_BELOW_HALF, + DOUBLE_NEGATIVE_HALF, + DOUBLE_NEGATIVE_ONE_AND_HALF, + DOUBLE_ONE_AND_HALF, + DOUBLE_TWO_AND_HALF, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +TRUNC_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_half", + expression={"$trunc": DOUBLE_HALF}, + expected=0.0, + msg="Should truncate double 0.5 down to 0.0 (no rounding)", + ), + ExpressionTestCase( + "double_one_and_half", + expression={"$trunc": DOUBLE_ONE_AND_HALF}, + expected=1.0, + msg="Should truncate double 1.5 down to 1.0 (no rounding)", + ), + ExpressionTestCase( + "double_two_and_half", + expression={"$trunc": DOUBLE_TWO_AND_HALF}, + expected=2.0, + msg="Should truncate double 2.5 down to 2.0 (no rounding)", + ), + ExpressionTestCase( + "double_negative_half", + expression={"$trunc": DOUBLE_NEGATIVE_HALF}, + expected=-0.0, + msg="Should truncate double -0.5 toward zero to -0.0 (no rounding)", + ), + ExpressionTestCase( + "double_negative_one_and_half", + expression={"$trunc": DOUBLE_NEGATIVE_ONE_AND_HALF}, + expected=-1.0, + msg="Should truncate double -1.5 toward zero to -1.0 (no rounding)", + ), + ExpressionTestCase( + "double_just_below_half", + expression={"$trunc": DOUBLE_JUST_BELOW_HALF}, + expected=0.0, + msg="Should truncate a double just below 0.5 to 0.0", + ), + ExpressionTestCase( + "double_just_above_half", + expression={"$trunc": DOUBLE_JUST_ABOVE_HALF}, + expected=0.0, + msg="Should truncate a double just above 0.5 to 0.0", + ), + ExpressionTestCase( + "decimal_half", + expression={"$trunc": DECIMAL128_HALF}, + expected=Decimal128("0"), + msg="Should truncate Decimal128 0.5 down to 0 (no rounding)", + ), + ExpressionTestCase( + "decimal_one_and_half", + expression={"$trunc": DECIMAL128_ONE_AND_HALF}, + expected=Decimal128("1"), + msg="Should truncate Decimal128 1.5 down to 1 (no rounding)", + ), + ExpressionTestCase( + "decimal_two_and_half", + expression={"$trunc": DECIMAL128_TWO_AND_HALF}, + expected=Decimal128("2"), + msg="Should truncate Decimal128 2.5 down to 2 (no rounding)", + ), + ExpressionTestCase( + "decimal_negative_half", + expression={"$trunc": DECIMAL128_NEGATIVE_HALF}, + expected=Decimal128("-0"), + msg="Should truncate Decimal128 -0.5 toward zero to -0 (no rounding)", + ), + ExpressionTestCase( + "decimal_negative_one_and_half", + expression={"$trunc": DECIMAL128_NEGATIVE_ONE_AND_HALF}, + expected=Decimal128("-1"), + msg="Should truncate Decimal128 -1.5 toward zero to -1 (no rounding)", + ), + ExpressionTestCase( + "decimal_just_below_half", + expression={"$trunc": DECIMAL128_JUST_BELOW_HALF}, + expected=Decimal128("0"), + msg="Should truncate a Decimal128 just below 0.5 to 0", + ), + ExpressionTestCase( + "decimal_just_above_half", + expression={"$trunc": DECIMAL128_JUST_ABOVE_HALF}, + expected=Decimal128("0"), + msg="Should truncate a Decimal128 just above 0.5 to 0", + ), + ExpressionTestCase( + "float_infinity", + expression={"$trunc": FLOAT_INFINITY}, + expected=FLOAT_INFINITY, + msg="Should preserve positive Infinity unchanged", + ), + ExpressionTestCase( + "float_negative_infinity", + expression={"$trunc": FLOAT_NEGATIVE_INFINITY}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="Should preserve negative Infinity unchanged", + ), + ExpressionTestCase( + "decimal128_infinity", + expression={"$trunc": DECIMAL128_INFINITY}, + expected=DECIMAL128_INFINITY, + msg="Should preserve Decimal128 positive Infinity unchanged", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + expression={"$trunc": DECIMAL128_NEGATIVE_INFINITY}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="Should preserve Decimal128 negative Infinity unchanged", + ), + ExpressionTestCase( + "float_nan", + expression={"$trunc": FLOAT_NAN}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should propagate float NaN unchanged", + ), + ExpressionTestCase( + "decimal128_nan", + expression={"$trunc": DECIMAL128_NAN}, + expected=DECIMAL128_NAN, + msg="Should propagate Decimal128 NaN unchanged", + ), + ExpressionTestCase( + "null_value", + expression={"$trunc": None}, + expected=None, + msg="Should return null for null input", + ), +] + +TRUNC_INSERT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_half", + expression={"$trunc": "$value"}, + doc={"value": DOUBLE_HALF}, + expected=0.0, + msg="Should truncate double 0.5 down to 0.0 (no rounding)", + ), + ExpressionTestCase( + "double_one_and_half", + expression={"$trunc": "$value"}, + doc={"value": DOUBLE_ONE_AND_HALF}, + expected=1.0, + msg="Should truncate double 1.5 down to 1.0 (no rounding)", + ), + ExpressionTestCase( + "double_two_and_half", + expression={"$trunc": "$value"}, + doc={"value": DOUBLE_TWO_AND_HALF}, + expected=2.0, + msg="Should truncate double 2.5 down to 2.0 (no rounding)", + ), + ExpressionTestCase( + "double_negative_half", + expression={"$trunc": "$value"}, + doc={"value": DOUBLE_NEGATIVE_HALF}, + expected=-0.0, + msg="Should truncate double -0.5 toward zero to -0.0 (no rounding)", + ), + ExpressionTestCase( + "double_negative_one_and_half", + expression={"$trunc": "$value"}, + doc={"value": DOUBLE_NEGATIVE_ONE_AND_HALF}, + expected=-1.0, + msg="Should truncate double -1.5 toward zero to -1.0 (no rounding)", + ), + ExpressionTestCase( + "double_just_below_half", + expression={"$trunc": "$value"}, + doc={"value": DOUBLE_JUST_BELOW_HALF}, + expected=0.0, + msg="Should truncate a double just below 0.5 to 0.0", + ), + ExpressionTestCase( + "double_just_above_half", + expression={"$trunc": "$value"}, + doc={"value": DOUBLE_JUST_ABOVE_HALF}, + expected=0.0, + msg="Should truncate a double just above 0.5 to 0.0", + ), + ExpressionTestCase( + "decimal_half", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_HALF}, + expected=Decimal128("0"), + msg="Should truncate Decimal128 0.5 down to 0 (no rounding)", + ), + ExpressionTestCase( + "decimal_one_and_half", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_ONE_AND_HALF}, + expected=Decimal128("1"), + msg="Should truncate Decimal128 1.5 down to 1 (no rounding)", + ), + ExpressionTestCase( + "decimal_two_and_half", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_TWO_AND_HALF}, + expected=Decimal128("2"), + msg="Should truncate Decimal128 2.5 down to 2 (no rounding)", + ), + ExpressionTestCase( + "decimal_negative_half", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_NEGATIVE_HALF}, + expected=Decimal128("-0"), + msg="Should truncate Decimal128 -0.5 toward zero to -0 (no rounding)", + ), + ExpressionTestCase( + "decimal_negative_one_and_half", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_NEGATIVE_ONE_AND_HALF}, + expected=Decimal128("-1"), + msg="Should truncate Decimal128 -1.5 toward zero to -1 (no rounding)", + ), + ExpressionTestCase( + "decimal_just_below_half", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_JUST_BELOW_HALF}, + expected=Decimal128("0"), + msg="Should truncate a Decimal128 just below 0.5 to 0", + ), + ExpressionTestCase( + "decimal_just_above_half", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_JUST_ABOVE_HALF}, + expected=Decimal128("0"), + msg="Should truncate a Decimal128 just above 0.5 to 0", + ), + ExpressionTestCase( + "float_infinity", + expression={"$trunc": "$value"}, + doc={"value": FLOAT_INFINITY}, + expected=FLOAT_INFINITY, + msg="Should preserve positive Infinity unchanged", + ), + ExpressionTestCase( + "float_negative_infinity", + expression={"$trunc": "$value"}, + doc={"value": FLOAT_NEGATIVE_INFINITY}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="Should preserve negative Infinity unchanged", + ), + ExpressionTestCase( + "decimal128_infinity", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_INFINITY}, + expected=DECIMAL128_INFINITY, + msg="Should preserve Decimal128 positive Infinity unchanged", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_NEGATIVE_INFINITY}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="Should preserve Decimal128 negative Infinity unchanged", + ), + ExpressionTestCase( + "float_nan", + expression={"$trunc": "$value"}, + doc={"value": FLOAT_NAN}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="Should propagate float NaN unchanged", + ), + ExpressionTestCase( + "decimal128_nan", + expression={"$trunc": "$value"}, + doc={"value": DECIMAL128_NAN}, + expected=DECIMAL128_NAN, + msg="Should propagate Decimal128 NaN unchanged", + ), + ExpressionTestCase( + "null_value", + expression={"$trunc": "$value"}, + doc={"value": None}, + expected=None, + msg="Should return null for null input", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TRUNC_LITERAL_TESTS)) +def test_trunc_literal(collection, test): + """Test $trunc with literal values""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) + + +@pytest.mark.parametrize("test", pytest_params(TRUNC_INSERT_TESTS)) +def test_trunc_insert(collection, test): + """Test $trunc with inserted document values""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) From 60dbcab95c737f7a8350801f2ba5315bc604d55e Mon Sep 17 00:00:00 2001 From: Victor Tsang Date: Wed, 22 Jul 2026 11:29:21 -0700 Subject: [PATCH 2/2] address reviewer comments Signed-off-by: Victor Tsang --- .../round/test_round_bson_type_validation.py | 54 +++++++++++++++--- .../trunc/test_trunc_bson_type_validation.py | 55 ++++++++++++++++--- .../arithmetic/trunc/test_trunc_errors.py | 14 +++++ 3 files changed, 105 insertions(+), 18 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_bson_type_validation.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_bson_type_validation.py index 8dbd8aae9..d1afda124 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_bson_type_validation.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/round/test_round_bson_type_validation.py @@ -1,12 +1,14 @@ """ BSON type validation tests for $round expression. -Systematically verifies that the number (first) input accepts the four numeric -types plus null, and rejects every other BSON type. The `place`-position -validation lives in test_round_errors.py. +Verifies the number (first) and place (second) inputs each accept the four +numeric types plus null, and reject every other BSON type. Value-level place +validation (out-of-range, non-integral, NaN/infinity) lives in +test_round_errors.py. """ import pytest +from bson import Decimal128, Int64 from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -19,7 +21,7 @@ generate_bson_acceptance_test_cases, generate_bson_rejection_test_cases, ) -from documentdb_tests.framework.error_codes import NON_NUMERIC_TYPE_ERROR +from documentdb_tests.framework.error_codes import INVALID_TYPE_ERROR, NON_NUMERIC_TYPE_ERROR ROUND_BSON_PARAMS = [ BsonTypeTestCase( @@ -36,17 +38,51 @@ ], default_error_code=NON_NUMERIC_TYPE_ERROR, ), + BsonTypeTestCase( + id="place", + msg="$round place should reject non-numeric types", + keyword="place", + # The four numeric types plus null (null place propagates to a null result). + valid_types=[ + BsonType.DOUBLE, + BsonType.INT, + BsonType.LONG, + BsonType.DECIMAL, + BsonType.NULL, + ], + default_error_code=INVALID_TYPE_ERROR, + valid_inputs={ + BsonType.DOUBLE: 2.0, + BsonType.INT: 2, + BsonType.LONG: Int64(2), + BsonType.DECIMAL: Decimal128("2"), + }, + ), ] REJECTION_CASES = generate_bson_rejection_test_cases(ROUND_BSON_PARAMS) ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(ROUND_BSON_PARAMS) +def _round_expression(spec): + """Build the $round expression for the argument under test.""" + if spec.keyword == "place": + return {"$round": ["$value", "$place"]} + return {"$round": "$value"} + + +def _round_document(spec, sample_value): + """Build the inserted document for the argument under test.""" + if spec.keyword == "place": + return {"value": 3.14159, "place": sample_value} + return {"value": sample_value} + + @pytest.mark.parametrize("bson_type,sample_value,spec", REJECTION_CASES) def test_round_bson_type_rejected(collection, bson_type, sample_value, spec): - """Verifies $round rejects invalid BSON types for the number input.""" + """Verifies $round rejects invalid BSON types for the number and place inputs.""" result = execute_expression_with_insert( - collection, {"$round": "$value"}, {"value": sample_value} + collection, _round_expression(spec), _round_document(spec, sample_value) ) assert_expression_result( result, @@ -57,8 +93,8 @@ def test_round_bson_type_rejected(collection, bson_type, sample_value, spec): @pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) def test_round_bson_type_accepted(collection, bson_type, sample_value, spec): - """Verifies $round accepts valid numeric BSON types (and null).""" + """Verifies $round accepts valid numeric BSON types (and null) for number and place.""" result = execute_expression_with_insert( - collection, {"$round": "$value"}, {"value": sample_value} + collection, _round_expression(spec), _round_document(spec, sample_value) ) - assertNotError(result, msg=f"$round number should accept {bson_type.value}") + assertNotError(result, msg=f"$round {spec.keyword} should accept {bson_type.value}") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_bson_type_validation.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_bson_type_validation.py index 529d55f45..03bf7f3de 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_bson_type_validation.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_bson_type_validation.py @@ -1,12 +1,14 @@ """ BSON type validation tests for $trunc expression. -Systematically verifies that the number (first) input accepts the four numeric -types plus null, and rejects every other BSON type. The `place`-position -validation lives in test_trunc_errors.py. +Verifies the number (first) and place (second) inputs each accept the four +numeric types plus null, and reject every other BSON type. Value-level place +validation (out-of-range, non-integral, NaN/infinity) lives in +test_trunc_errors.py. """ import pytest +from bson import Decimal128, Int64 from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( assert_expression_result, @@ -19,13 +21,14 @@ generate_bson_acceptance_test_cases, generate_bson_rejection_test_cases, ) -from documentdb_tests.framework.error_codes import NON_NUMERIC_TYPE_ERROR +from documentdb_tests.framework.error_codes import INVALID_TYPE_ERROR, NON_NUMERIC_TYPE_ERROR TRUNC_BSON_PARAMS = [ BsonTypeTestCase( id="number", msg="$trunc number should reject non-numeric types", keyword="number", + # The four numeric types plus null (null propagates to a null result). valid_types=[ BsonType.DOUBLE, BsonType.INT, @@ -35,17 +38,51 @@ ], default_error_code=NON_NUMERIC_TYPE_ERROR, ), + BsonTypeTestCase( + id="place", + msg="$trunc place should reject non-numeric types", + keyword="place", + # The four numeric types plus null (null place propagates to a null result). + valid_types=[ + BsonType.DOUBLE, + BsonType.INT, + BsonType.LONG, + BsonType.DECIMAL, + BsonType.NULL, + ], + default_error_code=INVALID_TYPE_ERROR, + valid_inputs={ + BsonType.DOUBLE: 2.0, + BsonType.INT: 2, + BsonType.LONG: Int64(2), + BsonType.DECIMAL: Decimal128("2"), + }, + ), ] REJECTION_CASES = generate_bson_rejection_test_cases(TRUNC_BSON_PARAMS) ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(TRUNC_BSON_PARAMS) +def _trunc_expression(spec): + """Build the $trunc expression for the argument under test.""" + if spec.keyword == "place": + return {"$trunc": ["$value", "$place"]} + return {"$trunc": "$value"} + + +def _trunc_document(spec, sample_value): + """Build the inserted document for the argument under test.""" + if spec.keyword == "place": + return {"value": 3.14159, "place": sample_value} + return {"value": sample_value} + + @pytest.mark.parametrize("bson_type,sample_value,spec", REJECTION_CASES) def test_trunc_bson_type_rejected(collection, bson_type, sample_value, spec): - """Verifies $trunc rejects invalid BSON types for the number input.""" + """Verifies $trunc rejects invalid BSON types for the number and place inputs.""" result = execute_expression_with_insert( - collection, {"$trunc": "$value"}, {"value": sample_value} + collection, _trunc_expression(spec), _trunc_document(spec, sample_value) ) assert_expression_result( result, @@ -56,8 +93,8 @@ def test_trunc_bson_type_rejected(collection, bson_type, sample_value, spec): @pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) def test_trunc_bson_type_accepted(collection, bson_type, sample_value, spec): - """Verifies $trunc accepts valid numeric BSON types (and null).""" + """Verifies $trunc accepts valid numeric BSON types (and null) for number and place.""" result = execute_expression_with_insert( - collection, {"$trunc": "$value"}, {"value": sample_value} + collection, _trunc_expression(spec), _trunc_document(spec, sample_value) ) - assertNotError(result, msg=f"$trunc number should accept {bson_type.value}") + assertNotError(result, msg=f"$trunc {spec.keyword} should accept {bson_type.value}") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_errors.py index 32ec449f4..177e33712 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/trunc/test_trunc_errors.py @@ -276,6 +276,20 @@ error_code=NON_NUMERIC_TYPE_ERROR, msg="Should reject composite array from $x.y on array-of-objects", ), + ExpressionTestCase( + "array_index_path", + expression={"$trunc": ["$arr.0", 1]}, + doc={"arr": [1.567, 2.345]}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Should reject $arr.0 on an array (no positional indexing)", + ), + ExpressionTestCase( + "array_index_on_object_key", + expression={"$trunc": ["$a.0.b", 1]}, + doc={"a": [{"b": 1.567}, {"b": 2.345}]}, + error_code=NON_NUMERIC_TYPE_ERROR, + msg="Should reject $a.0.b on array-of-objects (no positional indexing)", + ), ]