From 51a482ab4db4faa743125c3ac8699d126a8fbf33 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:32:35 -0700 Subject: [PATCH 01/10] Added tests Signed-off-by: PatersonProjects --- .../arithmetic/add/test_add_date.py | 162 +++++++++++ .../arithmetic/add/test_add_errors.py | 203 ++++++++++++++ .../arithmetic/add/test_add_input_forms.py | 47 ++++ .../arithmetic/add/test_add_non_finite.py | 154 +++++++++++ .../arithmetic/add/test_add_null.py | 86 ++++++ .../arithmetic/add/test_add_numeric.py | 259 ++++++++++++++++++ .../arithmetic/add/test_add_overflow.py | 95 +++++++ .../arithmetic/add/test_add_precision.py | 103 +++++++ .../arithmetic/add/test_add_return_type.py | 114 ++++++++ 9 files changed, 1223 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py new file mode 100644 index 000000000..7cd0f09f2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py @@ -0,0 +1,162 @@ +from datetime import datetime, timedelta, timezone + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Date Arithmetic]: $add accepts exactly one date operand and one or more numeric +# operands (in milliseconds). The date may appear in any position. +ADD_DATE_NUMERIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_int32", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 86400000}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 2, tzinfo=timezone.utc), + msg="$add should add int32 milliseconds to a date", + ), + ExpressionTestCase( + "date_int64", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Int64(86400000)}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 2, tzinfo=timezone.utc), + msg="$add should add int64 milliseconds to a date", + ), + ExpressionTestCase( + "date_decimal", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Decimal128("1.5")}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 2000, tzinfo=timezone.utc), + msg="$add should round a decimal128 fractional millisecond value when adding to a date", + ), + ExpressionTestCase( + "date_double_round_up", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 3000, tzinfo=timezone.utc), + msg="$add should round up a double fractional millisecond value (.5) when adding to a date", + ), + ExpressionTestCase( + "date_double_truncates", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 4.4}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 4000, tzinfo=timezone.utc), + msg="$add should truncate a double fractional millisecond value (<.5) when adding to a date", # noqa: E501 + ), +] + +# Property [Date Rounding Boundaries]: $add rounds fractional millisecond offsets using +# round-half-away-from-zero. Values with |frac| < 0.5 truncate toward zero; values with +# |frac| >= 0.5 round away from zero. +ADD_DATE_ROUNDING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_double_0_1", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.1}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should truncate 0.1ms and leave the date unchanged", + ), + ExpressionTestCase( + "date_double_0_49", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.49}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should truncate 0.49ms and leave the date unchanged", + ), + ExpressionTestCase( + "date_double_0_51", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.51}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), + msg="$add should round up 0.51ms to 1ms when adding to a date", + ), + ExpressionTestCase( + "date_double_0_6", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.6}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), + msg="$add should round up 0.6ms to 1ms when adding to a date", + ), + ExpressionTestCase( + "date_double_neg_0_5", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.5}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), + msg="$add should round -0.5ms away from zero to -1ms when adding to a date", + ), + ExpressionTestCase( + "date_double_neg_0_51", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.51}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), + msg="$add should round -0.51ms away from zero to -1ms when adding to a date", + ), +] + +# Property [Date Operand Position]: the date operand may appear in any position among the +# operands; only one date is permitted. +ADD_DATE_POSITION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "number_then_date", + doc={"a": 86400000, "b": datetime(2026, 1, 1, tzinfo=timezone.utc)}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 2, tzinfo=timezone.utc), + msg="$add should add a date when the numeric operand appears before the date", + ), + ExpressionTestCase( + "date_in_middle", + doc={"a": 1, "b": datetime(2026, 1, 1, tzinfo=timezone.utc), "c": 1000}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=datetime(2026, 1, 1, 0, 0, 1, 1000, tzinfo=timezone.utc), + msg="$add should add a date when it appears in the middle of the operand list", + ), +] + +# Property [Date Sign Handling]: adding zero or a negative number of milliseconds to a date +# returns the date unchanged or subtracted. +ADD_DATE_SIGN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_negative", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -86400000}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2025, 12, 31, tzinfo=timezone.utc), + msg="$add should subtract milliseconds from a date when adding a negative number", + ), + ExpressionTestCase( + "date_zero", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should return the same date when adding zero milliseconds", + ), + ExpressionTestCase( + "date_negative_zero", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.0}, + expression={"$add": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, tzinfo=timezone.utc), + msg="$add should return the same date when adding negative zero", + ), +] + +ADD_DATE_ALL_TESTS = ( + ADD_DATE_NUMERIC_TESTS + ADD_DATE_POSITION_TESTS + ADD_DATE_SIGN_TESTS + ADD_DATE_ROUNDING_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_DATE_ALL_TESTS)) +def test_add_date(collection, test_case: ExpressionTestCase): + """Test $add date arithmetic: numeric types, operand position, and sign handling.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py new file mode 100644 index 000000000..f5c817b3d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py @@ -0,0 +1,203 @@ +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + MORE_THAN_ONE_DATE_ERROR, + OVERFLOW_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + FLOAT_INFINITY, + FLOAT_NAN, + INT64_MAX, +) + +# Property [Type Strictness]: $add rejects non-numeric, non-date operand types. +ADD_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + f"type_{tid}", + doc={"a": 1, "b": val}, + expression={"$add": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"$add should reject a {tid} operand", + ) + for tid, val in [ + ("string", "string"), + ("bool", True), + ("array", [2, 3]), + ("object", {"a": 2}), + ("regex", Regex("abc")), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("binary", Binary(b"data")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("timestamp", Timestamp(1, 1)), + ("code", Code("function(){}")), + ] +] + +# Property [Mixed Valid and Invalid]: $add rejects an invalid operand when it appears among +# valid numeric operands. +ADD_MIXED_VALID_INVALID_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "mixed_valid_invalid", + doc={"a": 1, "b": 2, "c": "string"}, + expression={"$add": ["$a", "$b", "$c"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should error when a string appears among numeric operands", + ), +] + +# Property [Single Invalid Operand]: $add rejects a single operand of an invalid type. +ADD_SINGLE_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "single_string", + doc={"a": "string"}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single string operand", + ), + ExpressionTestCase( + "single_boolean", + doc={"a": True}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single boolean operand", + ), + ExpressionTestCase( + "single_array", + doc={"a": [1, 2]}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single array operand", + ), + ExpressionTestCase( + "single_object", + doc={"a": {"x": 1}}, + expression={"$add": ["$a"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$add should reject a single object operand", + ), +] + +# Property [Multiple Dates]: $add rejects expressions with more than one date operand. +ADD_MULTIPLE_DATE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "add_two_identical_dates", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 1, tzinfo=timezone.utc), + }, + expression={"$add": ["$a", "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when adding two identical date operands", + ), + ExpressionTestCase( + "two_different_dates", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 2, tzinfo=timezone.utc), + }, + expression={"$add": ["$a", "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when adding two different date operands", + ), + ExpressionTestCase( + "two_dates_with_numbers", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 2, tzinfo=timezone.utc), + }, + expression={"$add": [1, 2, 3, "$a", "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when two dates appear among numeric operands", + ), + ExpressionTestCase( + "dates_separated_by_number", + doc={ + "a": datetime(2026, 1, 1, tzinfo=timezone.utc), + "b": datetime(2026, 1, 2, tzinfo=timezone.utc), + }, + expression={"$add": ["$a", 1, "$b"]}, + error_code=MORE_THAN_ONE_DATE_ERROR, + msg="$add should error when two dates are separated by a numeric operand", + ), +] + +# Property [Date with Non-Finite]: $add rejects NaN and Infinity as numeric operands when a +# date is also present, since the resulting date would be non-representable. +ADD_DATE_NON_FINITE_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_nan", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_NAN}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and float NaN", + ), + ExpressionTestCase( + "date_infinity", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_INFINITY}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and float infinity", + ), + ExpressionTestCase( + "date_decimal_nan", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_NAN}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and decimal128 NaN", + ), + ExpressionTestCase( + "date_decimal_infinity", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_INFINITY}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding a date and decimal128 infinity", + ), +] + +# Property [Date Overflow]: $add errors when the millisecond offset would push the date result +# beyond the representable date range. +ADD_DATE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_int64_max", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": INT64_MAX}, + expression={"$add": ["$a", "$b"]}, + error_code=OVERFLOW_ERROR, + msg="$add should error when adding INT64_MAX milliseconds to a date overflows the date range", # noqa: E501 + ), +] + +ADD_ERROR_ALL_TESTS = ( + ADD_TYPE_ERROR_TESTS + + ADD_MIXED_VALID_INVALID_TESTS + + ADD_SINGLE_TYPE_ERROR_TESTS + + ADD_MULTIPLE_DATE_TESTS + + ADD_DATE_NON_FINITE_ERROR_TESTS + + ADD_DATE_OVERFLOW_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_ERROR_ALL_TESTS)) +def test_add_errors(collection, test_case: ExpressionTestCase): + """Test $add type, multiple-date, and date non-finite error cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py new file mode 100644 index 000000000..7019c1b0b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py @@ -0,0 +1,47 @@ +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Expression Input]: $add evaluates a nested expression argument before summing. +ADD_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_add", + doc={"a": 1, "b": 2}, + expression={"$add": [{"$add": ["$a", "$b"]}, 3]}, + expected=6, + msg="$add should evaluate a nested $add expression as an operand", + ), +] + +# Property [Mixed Literal and Field]: $add accepts a mix of field references and inline literals +# in the same operand list. +ADD_MIXED_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "mixed_literal_and_field", + doc={"a": 10}, + expression={"$add": ["$a", 5]}, + expected=15, + msg="$add should sum a field reference and an inline literal operand", + ), +] + +ADD_INPUT_FORM_ALL_TESTS = ADD_EXPRESSION_INPUT_TESTS + ADD_MIXED_INPUT_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_INPUT_FORM_ALL_TESTS)) +def test_add_input_forms(collection, test_case: ExpressionTestCase): + """Test $add literal, nested expression, and mixed input form cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py new file mode 100644 index 000000000..0343c3316 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py @@ -0,0 +1,154 @@ +import math + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +# Property [Infinity]: $add propagates infinity according to IEEE 754 rules. +ADD_INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "infinity", + doc={"a": FLOAT_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity when adding infinity and a finite number", + ), + ExpressionTestCase( + "negative_infinity", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity when adding negative infinity and a finite number", # noqa: E501 + ), + ExpressionTestCase( + "single_infinity", + doc={"a": FLOAT_INFINITY}, + expression={"$add": ["$a"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity for a single infinity operand", + ), + ExpressionTestCase( + "inf_plus_inf", + doc={"a": FLOAT_INFINITY, "b": FLOAT_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity when adding two positive infinities", + ), + ExpressionTestCase( + "neg_inf_plus_neg_inf", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity when adding two negative infinities", + ), + ExpressionTestCase( + "inf_plus_zero", + doc={"a": FLOAT_INFINITY, "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return infinity when adding infinity and zero", + ), + ExpressionTestCase( + "neg_inf_plus_zero", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity when adding negative infinity and zero", + ), + ExpressionTestCase( + "decimal_infinity", + doc={"a": DECIMAL128_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_INFINITY, + msg="$add should return decimal128 infinity when adding decimal128 infinity and a number", + ), + ExpressionTestCase( + "decimal_negative_infinity", + doc={"a": DECIMAL128_NEGATIVE_INFINITY, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="$add should return decimal128 negative infinity when adding decimal128 negative infinity and a number", # noqa: E501 + ), +] + +# Property [NaN]: $add propagates NaN according to IEEE 754 rules. +ADD_NAN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nan_add_one", + doc={"a": FLOAT_NAN, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding float NaN and a finite number", + ), + ExpressionTestCase( + "inf_minus_inf", + doc={"a": FLOAT_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding float infinity and negative infinity", + ), + ExpressionTestCase( + "nan_plus_nan", + doc={"a": FLOAT_NAN, "b": FLOAT_NAN}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding two float NaN values", + ), + ExpressionTestCase( + "nan_plus_inf", + doc={"a": FLOAT_NAN, "b": FLOAT_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$add should return NaN when adding float NaN and infinity", + ), + ExpressionTestCase( + "decimal_nan", + doc={"a": DECIMAL128_NAN, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$add should return decimal128 NaN when adding decimal128 NaN and a number", + ), + ExpressionTestCase( + "decimal_nan_plus_nan", + doc={"a": DECIMAL128_NAN, "b": DECIMAL128_NAN}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$add should return decimal128 NaN when adding two decimal128 NaN values", + ), + ExpressionTestCase( + "decimal_inf_minus_inf", + doc={"a": DECIMAL128_INFINITY, "b": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$add should return decimal128 NaN when adding decimal128 infinity and negative infinity", # noqa: E501 + ), +] + +ADD_NON_FINITE_ALL_TESTS = ADD_INFINITY_TESTS + ADD_NAN_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_NON_FINITE_ALL_TESTS)) +def test_add_non_finite(collection, test_case: ExpressionTestCase): + """Test $add infinity and NaN propagation cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py new file mode 100644 index 000000000..9651d52d3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py @@ -0,0 +1,86 @@ +from datetime import datetime, timezone + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import MISSING + +# Property [Null Propagation]: $add returns null if any operand is null or refers to a missing +# field. +ADD_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "single_null", + doc={"a": None}, + expression={"$add": ["$a"]}, + expected=None, + msg="$add should return null for a single null operand", + ), + ExpressionTestCase( + "null_operand", + doc={"a": 1, "b": None}, + expression={"$add": ["$a", "$b"]}, + expected=None, + msg="$add should return null when any operand is null", + ), + ExpressionTestCase( + "missing_field", + doc={}, + expression={"$add": [1, MISSING]}, + expected=None, + msg="$add should return null when any operand is a missing field", + ), + ExpressionTestCase( + "null_with_multiple", + doc={"a": 1, "b": 2, "c": None}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=None, + msg="$add should return null when null appears among multiple operands", + ), + ExpressionTestCase( + "null_in_middle", + doc={"a": 1, "b": 2, "c": 3, "e": 5}, + expression={"$add": ["$a", "$b", "$c", None, "$e"]}, + expected=None, + msg="$add should return null when null appears in the middle of operands", + ), + ExpressionTestCase( + "all_null", + doc={"a": None, "b": None}, + expression={"$add": ["$a", "$b"]}, + expected=None, + msg="$add should return null when all operands are null", + ), + ExpressionTestCase( + "all_missing", + doc={}, + expression={"$add": [MISSING, MISSING]}, + expected=None, + msg="$add should return null when all operands are missing fields", + ), + ExpressionTestCase( + "date_and_null", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": None}, + expression={"$add": ["$a", "$b"]}, + expected=None, + msg="$add should return null when a date is paired with a null operand", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_NULL_TESTS)) +def test_add_null(collection, test_case: ExpressionTestCase): + """Test $add null and missing field propagation cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py new file mode 100644 index 000000000..3c4d6f305 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py @@ -0,0 +1,259 @@ +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Same-Type Addition]: $add of two values of the same numeric type returns a value of +# that type. +ADD_SAME_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "same_type_int32", + doc={"a": 1, "b": 2}, + expression={"$add": ["$a", "$b"]}, + expected=3, + msg="$add should add two int32 values", + ), + ExpressionTestCase( + "same_type_int64", + doc={"a": Int64(10), "b": Int64(20)}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(30), + msg="$add should add two int64 values", + ), + ExpressionTestCase( + "same_type_double", + doc={"a": 1.5, "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=4.0, + msg="$add should add two double values", + ), + ExpressionTestCase( + "same_type_decimal", + doc={"a": Decimal128("10.5"), "b": Decimal128("20.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("31.0"), + msg="$add should add two decimal128 values", + ), +] + +# Property [Type Promotion]: $add promotes to the wider type when operands have different numeric +# types. Precedence: decimal128 > double > int64 > int32. +ADD_MIXED_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_int64", + doc={"a": 1, "b": Int64(20)}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(21), + msg="$add should return int64 when adding int32 and int64", + ), + ExpressionTestCase( + "int32_double", + doc={"a": 1, "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=3.5, + msg="$add should return double when adding int32 and double", + ), + ExpressionTestCase( + "int32_decimal", + doc={"a": 1, "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("3.5"), + msg="$add should return decimal128 when adding int32 and decimal128", + ), + ExpressionTestCase( + "int64_double", + doc={"a": Int64(10), "b": 2.5}, + expression={"$add": ["$a", "$b"]}, + expected=12.5, + msg="$add should return double when adding int64 and double", + ), + ExpressionTestCase( + "int64_decimal", + doc={"a": Int64(10), "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("12.5"), + msg="$add should return decimal128 when adding int64 and decimal128", + ), + ExpressionTestCase( + "double_decimal", + doc={"a": 1.5, "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(Decimal128("4.00000000000000")), + msg="$add should return decimal128 when adding double and decimal128", + ), + ExpressionTestCase( + "three_mixed_types", + doc={"a": Decimal128("1.5"), "b": 2.5, "c": Int64(3)}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=pytest.approx(Decimal128("7.00000000000000")), + msg="$add should return decimal128 when adding decimal128, double, and int64", + ), +] + +# Property [Multiple Operands]: $add correctly sums three or more operands. +ADD_MULTIPLE_OPERANDS_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "multiple_int32", + doc={"a": 1, "b": 2, "c": 3}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=6, + msg="$add should add multiple int32 values", + ), + ExpressionTestCase( + "multiple_int64", + doc={"a": Int64(1), "b": Int64(2), "c": Int64(3)}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=Int64(6), + msg="$add should add multiple int64 values", + ), + ExpressionTestCase( + "multiple_double", + doc={"a": 1.1, "b": 2.2, "c": 3.3}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=pytest.approx(6.6), + msg="$add should add multiple double values", + ), + ExpressionTestCase( + "multiple_decimal", + doc={ + "a": Decimal128("1"), + "b": Decimal128("2"), + "c": Decimal128("3"), + "d": Decimal128("4"), + }, + expression={"$add": ["$a", "$b", "$c", "$d"]}, + expected=Decimal128("10"), + msg="$add should add multiple decimal128 values", + ), + ExpressionTestCase( + "five_operands", + doc={"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, + expression={"$add": ["$a", "$b", "$c", "$d", "$e"]}, + expected=15, + msg="$add should correctly sum five int32 operands", + ), + ExpressionTestCase( + "ten_operands", + doc={ + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6, + "g": 7, + "h": 8, + "i": 9, + "j": 10, + }, + expression={"$add": ["$a", "$b", "$c", "$d", "$e", "$f", "$g", "$h", "$i", "$j"]}, + expected=55, + msg="$add should correctly sum ten int32 operands", + ), +] + +# Property [Empty and Single Operand]: $add of zero operands returns 0; single operand returns +# that value unchanged. +ADD_SINGLE_AND_EMPTY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty", + doc={}, + expression={"$add": []}, + expected=0, + msg="$add should return 0 for empty operand list", + ), + ExpressionTestCase( + "single_int32", + doc={"a": 5}, + expression={"$add": ["$a"]}, + expected=5, + msg="$add should return the value for a single int32 operand", + ), + ExpressionTestCase( + "single_int64", + doc={"a": Int64(0)}, + expression={"$add": ["$a"]}, + expected=Int64(0), + msg="$add should return the value for a single int64 operand", + ), + ExpressionTestCase( + "single_double", + doc={"a": 0.0}, + expression={"$add": ["$a"]}, + expected=0.0, + msg="$add should return the value for a single double operand", + ), + ExpressionTestCase( + "single_decimal", + doc={"a": Decimal128("0")}, + expression={"$add": ["$a"]}, + expected=Decimal128("0"), + msg="$add should return the value for a single decimal128 operand", + ), +] + +# Property [Sign Handling]: $add handles negative values and zero correctly. +ADD_SIGN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "negative_positive", + doc={"a": -5, "b": 3}, + expression={"$add": ["$a", "$b"]}, + expected=-2, + msg="$add should add a negative and a positive int32 value", + ), + ExpressionTestCase( + "both_negative", + doc={"a": -10, "b": -20}, + expression={"$add": ["$a", "$b"]}, + expected=-30, + msg="$add should add two negative int32 values", + ), + ExpressionTestCase( + "zeros", + doc={"a": 0, "b": 0}, + expression={"$add": ["$a", "$b"]}, + expected=0, + msg="$add should return 0 when adding two int32 zeros", + ), + ExpressionTestCase( + "zero_negative_zero", + doc={"a": 0, "b": -0.0}, + expression={"$add": ["$a", "$b"]}, + expected=0.0, + msg="$add should return 0.0 when adding int32 zero and negative zero double", + ), + ExpressionTestCase( + "sum_to_zero", + doc={"a": 1, "b": 0, "c": -1}, + expression={"$add": ["$a", "$b", "$c"]}, + expected=0, + msg="$add should return 0 when operands sum to zero", + ), +] + +ADD_NUMERIC_ALL_TESTS = ( + ADD_SAME_TYPE_TESTS + + ADD_MIXED_TYPE_TESTS + + ADD_MULTIPLE_OPERANDS_TESTS + + ADD_SINGLE_AND_EMPTY_TESTS + + ADD_SIGN_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_NUMERIC_ALL_TESTS)) +def test_add_numeric(collection, test_case: ExpressionTestCase): + """Test $add numeric type combinations, multiple operands, and sign handling.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py new file mode 100644 index 000000000..29a992905 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py @@ -0,0 +1,95 @@ +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DOUBLE_FROM_INT64_MAX, + FLOAT_INFINITY, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT32_OVERFLOW, + INT32_UNDERFLOW, + INT64_MAX, + INT64_MIN, +) + +# Property [Int32 Overflow]: when an int32 result exceeds the int32 range, $add promotes to +# int64. +ADD_INT32_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_overflow", + doc={"a": INT32_MAX, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(INT32_OVERFLOW), + msg="$add should promote to int64 when the int32 result overflows INT32_MAX", + ), + ExpressionTestCase( + "int32_underflow", + doc={"a": INT32_MIN, "b": -1}, + expression={"$add": ["$a", "$b"]}, + expected=Int64(INT32_UNDERFLOW), + msg="$add should promote to int64 when the int32 result underflows INT32_MIN", + ), +] + +# Property [Int64 Overflow]: when an int64 result exceeds the int64 range, $add promotes to +# double. +ADD_INT64_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int64_overflow", + doc={"a": INT64_MAX, "b": 1}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(DOUBLE_FROM_INT64_MAX), + msg="$add should promote to double when the int64 result overflows INT64_MAX", + ), + ExpressionTestCase( + "int64_underflow", + doc={"a": INT64_MIN, "b": -1}, + expression={"$add": ["$a", "$b"]}, + expected=pytest.approx(-DOUBLE_FROM_INT64_MAX), + msg="$add should promote to double when the int64 result underflows INT64_MIN", + ), +] + +# Property [Double Overflow]: when a double result exceeds the double range, $add returns +# infinity. +ADD_DOUBLE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_overflow", + doc={"a": 1e308, "b": 1e308}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$add should return positive infinity on double overflow", + ), + ExpressionTestCase( + "double_underflow", + doc={"a": -1e308, "b": -1e308}, + expression={"$add": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$add should return negative infinity on double underflow", + ), +] + +ADD_OVERFLOW_ALL_TESTS = ( + ADD_INT32_OVERFLOW_TESTS + ADD_INT64_OVERFLOW_TESTS + ADD_DOUBLE_OVERFLOW_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_OVERFLOW_ALL_TESTS)) +def test_add_overflow(collection, test_case: ExpressionTestCase): + """Test $add integer and double overflow and underflow cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py new file mode 100644 index 000000000..f0cdd34ab --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py @@ -0,0 +1,103 @@ +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_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_NEGATIVE_INFINITY, +) + +# Property [Decimal128 Precision]: $add preserves decimal128 precision, including exact +# representation of values that are inexact in double. +ADD_DECIMAL_PRECISION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_precision", + doc={"a": Decimal128("1.5"), "b": Decimal128("2.5")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("4.0"), + msg="$add should preserve decimal128 precision", + ), + ExpressionTestCase( + "decimal_precision_small", + doc={"a": Decimal128("0.1"), "b": Decimal128("0.2")}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("0.3"), + msg="$add should exactly represent 0.1 + 0.2 with decimal128", + ), + ExpressionTestCase( + "decimal_large_precision", + doc={ + "a": Decimal128("999999999999999999999999999999999"), + "b": Decimal128("1"), + }, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("1000000000000000000000000000000000"), + msg="$add should handle large decimal128 addition with full precision", + ), + ExpressionTestCase( + "decimal_large_negative_precision", + doc={ + "a": Decimal128("-999999999999999999999999999999999"), + "b": Decimal128("-1"), + }, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("-1000000000000000000000000000000000"), + msg="$add should handle large negative decimal128 addition with full precision", + ), +] + +# Property [Decimal128 Boundaries]: $add at decimal128 boundary values promotes to infinity when +# the result overflows, and returns zero when max and min cancel. +ADD_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal128_max_plus_zero", + doc={"a": DECIMAL128_MAX, "b": Decimal128("0")}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_MAX, + msg="$add should return decimal128 max when adding zero to decimal128 max", + ), + ExpressionTestCase( + "decimal128_max_plus_max", + doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MAX}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_INFINITY, + msg="$add should return decimal128 infinity when adding two decimal128 max values", + ), + ExpressionTestCase( + "decimal128_min_plus_min", + doc={"a": DECIMAL128_MIN, "b": DECIMAL128_MIN}, + expression={"$add": ["$a", "$b"]}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="$add should return decimal128 negative infinity when adding two decimal128 min values", + ), + ExpressionTestCase( + "decimal128_max_plus_min", + doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MIN}, + expression={"$add": ["$a", "$b"]}, + expected=Decimal128("0E+6111"), + msg="$add should return zero when adding decimal128 max and decimal128 min", + ), +] + +ADD_PRECISION_ALL_TESTS = ADD_DECIMAL_PRECISION_TESTS + ADD_DECIMAL_BOUNDARY_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_PRECISION_ALL_TESTS)) +def test_add_precision(collection, test_case: ExpressionTestCase): + """Test $add decimal128 precision and boundary value cases.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py new file mode 100644 index 000000000..7af9e0b66 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py @@ -0,0 +1,114 @@ +from datetime import datetime, timezone + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Return Type]: $add follows numeric type promotion rules to determine the result type. +# Precedence: decimal128 > double > int64 > int32. Date + numeric always returns date. +ADD_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type_int_int", + doc={"a": 1, "b": 2}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="int", + msg="$add should return int type when adding two int32 values", + ), + ExpressionTestCase( + "return_type_int_long", + doc={"a": 1, "b": Int64(2)}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="long", + msg="$add should return long type when adding int32 and int64", + ), + ExpressionTestCase( + "return_type_int_double", + doc={"a": 1, "b": 2.0}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="double", + msg="$add should return double type when adding int32 and double", + ), + ExpressionTestCase( + "return_type_int_decimal", + doc={"a": 1, "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding int32 and decimal128", + ), + ExpressionTestCase( + "return_type_long_long", + doc={"a": Int64(1), "b": Int64(2)}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="long", + msg="$add should return long type when adding two int64 values", + ), + ExpressionTestCase( + "return_type_long_double", + doc={"a": Int64(1), "b": 2.0}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="double", + msg="$add should return double type when adding int64 and double", + ), + ExpressionTestCase( + "return_type_long_decimal", + doc={"a": Int64(1), "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding int64 and decimal128", + ), + ExpressionTestCase( + "return_type_double_double", + doc={"a": 1.0, "b": 2.0}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="double", + msg="$add should return double type when adding two double values", + ), + ExpressionTestCase( + "return_type_double_decimal", + doc={"a": 1.0, "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding double and decimal128", + ), + ExpressionTestCase( + "return_type_decimal_decimal", + doc={"a": Decimal128("1"), "b": Decimal128("2")}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="decimal", + msg="$add should return decimal type when adding two decimal128 values", + ), + ExpressionTestCase( + "return_type_date_int", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 1000}, + expression={"$type": {"$add": ["$a", "$b"]}}, + expected="date", + msg="$add should return date type when adding a date and an int32", + ), + ExpressionTestCase( + "return_type_empty", + doc={}, + expression={"$type": {"$add": []}}, + expected="int", + msg="$add should return int type for an empty operand list", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(ADD_RETURN_TYPE_TESTS)) +def test_add_return_type(collection, test_case: ExpressionTestCase): + """Test $add return type promotion rules for all numeric type combinations.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) From 014f2bceb8c06ba36a9e652a57ac7d81397bd67e Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:40:50 -0700 Subject: [PATCH 02/10] Added docStrings to all files Signed-off-by: PatersonProjects --- .../core/operator/expressions/arithmetic/add/test_add_date.py | 4 ++++ .../operator/expressions/arithmetic/add/test_add_errors.py | 2 ++ .../expressions/arithmetic/add/test_add_input_forms.py | 2 ++ .../expressions/arithmetic/add/test_add_non_finite.py | 2 ++ .../core/operator/expressions/arithmetic/add/test_add_null.py | 2 ++ .../operator/expressions/arithmetic/add/test_add_numeric.py | 4 ++++ .../operator/expressions/arithmetic/add/test_add_overflow.py | 2 ++ .../operator/expressions/arithmetic/add/test_add_precision.py | 2 ++ .../expressions/arithmetic/add/test_add_return_type.py | 2 ++ 9 files changed, 22 insertions(+) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py index 7cd0f09f2..eff8f4af1 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py @@ -1,3 +1,7 @@ +"""Tests for $add date arithmetic including numeric offsets, rounding boundaries, operand +position, and sign handling. +""" + from datetime import datetime, timedelta, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py index f5c817b3d..127f0e5de 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py @@ -1,3 +1,5 @@ +"""Tests for $add error cases including invalid operand types, multiple dates, and date overflow.""" + from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py index 7019c1b0b..fa2f73df4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py @@ -1,3 +1,5 @@ +"""Tests for $add input forms including nested expressions and mixed literal/field operands.""" + import pytest from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py index 0343c3316..958818e98 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py @@ -1,3 +1,5 @@ +"""Tests for $add infinity and NaN propagation.""" + import math import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py index 9651d52d3..3854d72d8 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py @@ -1,3 +1,5 @@ +"""Tests for $add null and missing field propagation.""" + from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py index 3c4d6f305..971b01581 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py @@ -1,3 +1,7 @@ +"""Tests for $add numeric operations including same-type and mixed-type addition, multiple +operands, empty/single operands, and sign handling. +""" + import pytest from bson import Decimal128, Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py index 29a992905..2da84306e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py @@ -1,3 +1,5 @@ +"""Tests for $add integer and double overflow and underflow promotion.""" + import pytest from bson import Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py index f0cdd34ab..6658eb03a 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py @@ -1,3 +1,5 @@ +"""Tests for $add decimal128 precision and boundary value handling.""" + import pytest from bson import Decimal128 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py index 7af9e0b66..d681c7fd2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py @@ -1,3 +1,5 @@ +"""Tests for $add return type promotion rules across numeric and date operand combinations.""" + from datetime import datetime, timezone import pytest From 37407ae57c9912cf17ff31f5885fafea01f7db38 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:47:01 -0700 Subject: [PATCH 03/10] Revert "Added docStrings to all files" This reverts commit 014f2bceb8c06ba36a9e652a57ac7d81397bd67e. Signed-off-by: PatersonProjects --- .../core/operator/expressions/arithmetic/add/test_add_date.py | 4 ---- .../operator/expressions/arithmetic/add/test_add_errors.py | 2 -- .../expressions/arithmetic/add/test_add_input_forms.py | 2 -- .../expressions/arithmetic/add/test_add_non_finite.py | 2 -- .../core/operator/expressions/arithmetic/add/test_add_null.py | 2 -- .../operator/expressions/arithmetic/add/test_add_numeric.py | 4 ---- .../operator/expressions/arithmetic/add/test_add_overflow.py | 2 -- .../operator/expressions/arithmetic/add/test_add_precision.py | 2 -- .../expressions/arithmetic/add/test_add_return_type.py | 2 -- 9 files changed, 22 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py index eff8f4af1..7cd0f09f2 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py @@ -1,7 +1,3 @@ -"""Tests for $add date arithmetic including numeric offsets, rounding boundaries, operand -position, and sign handling. -""" - from datetime import datetime, timedelta, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py index 127f0e5de..f5c817b3d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py @@ -1,5 +1,3 @@ -"""Tests for $add error cases including invalid operand types, multiple dates, and date overflow.""" - from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py index fa2f73df4..7019c1b0b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py @@ -1,5 +1,3 @@ -"""Tests for $add input forms including nested expressions and mixed literal/field operands.""" - import pytest from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py index 958818e98..0343c3316 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py @@ -1,5 +1,3 @@ -"""Tests for $add infinity and NaN propagation.""" - import math import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py index 3854d72d8..9651d52d3 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py @@ -1,5 +1,3 @@ -"""Tests for $add null and missing field propagation.""" - from datetime import datetime, timezone import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py index 971b01581..3c4d6f305 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py @@ -1,7 +1,3 @@ -"""Tests for $add numeric operations including same-type and mixed-type addition, multiple -operands, empty/single operands, and sign handling. -""" - import pytest from bson import Decimal128, Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py index 2da84306e..29a992905 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py @@ -1,5 +1,3 @@ -"""Tests for $add integer and double overflow and underflow promotion.""" - import pytest from bson import Int64 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py index 6658eb03a..f0cdd34ab 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py @@ -1,5 +1,3 @@ -"""Tests for $add decimal128 precision and boundary value handling.""" - import pytest from bson import Decimal128 diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py index d681c7fd2..7af9e0b66 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py @@ -1,5 +1,3 @@ -"""Tests for $add return type promotion rules across numeric and date operand combinations.""" - from datetime import datetime, timezone import pytest From f851712aaa40362d3ada515b2c50fd52ffc9b3c4 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:47:24 -0700 Subject: [PATCH 04/10] Revert "Added tests" This reverts commit 51a482ab4db4faa743125c3ac8699d126a8fbf33. Signed-off-by: PatersonProjects --- .../arithmetic/add/test_add_date.py | 162 ----------- .../arithmetic/add/test_add_errors.py | 203 -------------- .../arithmetic/add/test_add_input_forms.py | 47 ---- .../arithmetic/add/test_add_non_finite.py | 154 ----------- .../arithmetic/add/test_add_null.py | 86 ------ .../arithmetic/add/test_add_numeric.py | 259 ------------------ .../arithmetic/add/test_add_overflow.py | 95 ------- .../arithmetic/add/test_add_precision.py | 103 ------- .../arithmetic/add/test_add_return_type.py | 114 -------- 9 files changed, 1223 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py deleted file mode 100644 index 7cd0f09f2..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_date.py +++ /dev/null @@ -1,162 +0,0 @@ -from datetime import datetime, timedelta, timezone - -import pytest -from bson import Decimal128, Int64 - -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 - ExpressionTestCase, -) -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( - assert_expression_result, - execute_expression_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Date Arithmetic]: $add accepts exactly one date operand and one or more numeric -# operands (in milliseconds). The date may appear in any position. -ADD_DATE_NUMERIC_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_int32", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 86400000}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 2, tzinfo=timezone.utc), - msg="$add should add int32 milliseconds to a date", - ), - ExpressionTestCase( - "date_int64", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Int64(86400000)}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 2, tzinfo=timezone.utc), - msg="$add should add int64 milliseconds to a date", - ), - ExpressionTestCase( - "date_decimal", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": Decimal128("1.5")}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 2000, tzinfo=timezone.utc), - msg="$add should round a decimal128 fractional millisecond value when adding to a date", - ), - ExpressionTestCase( - "date_double_round_up", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 3000, tzinfo=timezone.utc), - msg="$add should round up a double fractional millisecond value (.5) when adding to a date", - ), - ExpressionTestCase( - "date_double_truncates", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 4.4}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 4000, tzinfo=timezone.utc), - msg="$add should truncate a double fractional millisecond value (<.5) when adding to a date", # noqa: E501 - ), -] - -# Property [Date Rounding Boundaries]: $add rounds fractional millisecond offsets using -# round-half-away-from-zero. Values with |frac| < 0.5 truncate toward zero; values with -# |frac| >= 0.5 round away from zero. -ADD_DATE_ROUNDING_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_double_0_1", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.1}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should truncate 0.1ms and leave the date unchanged", - ), - ExpressionTestCase( - "date_double_0_49", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.49}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should truncate 0.49ms and leave the date unchanged", - ), - ExpressionTestCase( - "date_double_0_51", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.51}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), - msg="$add should round up 0.51ms to 1ms when adding to a date", - ), - ExpressionTestCase( - "date_double_0_6", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0.6}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), - msg="$add should round up 0.6ms to 1ms when adding to a date", - ), - ExpressionTestCase( - "date_double_neg_0_5", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.5}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), - msg="$add should round -0.5ms away from zero to -1ms when adding to a date", - ), - ExpressionTestCase( - "date_double_neg_0_51", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.51}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc) - timedelta(milliseconds=1), - msg="$add should round -0.51ms away from zero to -1ms when adding to a date", - ), -] - -# Property [Date Operand Position]: the date operand may appear in any position among the -# operands; only one date is permitted. -ADD_DATE_POSITION_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "number_then_date", - doc={"a": 86400000, "b": datetime(2026, 1, 1, tzinfo=timezone.utc)}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 2, tzinfo=timezone.utc), - msg="$add should add a date when the numeric operand appears before the date", - ), - ExpressionTestCase( - "date_in_middle", - doc={"a": 1, "b": datetime(2026, 1, 1, tzinfo=timezone.utc), "c": 1000}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=datetime(2026, 1, 1, 0, 0, 1, 1000, tzinfo=timezone.utc), - msg="$add should add a date when it appears in the middle of the operand list", - ), -] - -# Property [Date Sign Handling]: adding zero or a negative number of milliseconds to a date -# returns the date unchanged or subtracted. -ADD_DATE_SIGN_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_negative", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -86400000}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2025, 12, 31, tzinfo=timezone.utc), - msg="$add should subtract milliseconds from a date when adding a negative number", - ), - ExpressionTestCase( - "date_zero", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should return the same date when adding zero milliseconds", - ), - ExpressionTestCase( - "date_negative_zero", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": -0.0}, - expression={"$add": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, tzinfo=timezone.utc), - msg="$add should return the same date when adding negative zero", - ), -] - -ADD_DATE_ALL_TESTS = ( - ADD_DATE_NUMERIC_TESTS + ADD_DATE_POSITION_TESTS + ADD_DATE_SIGN_TESTS + ADD_DATE_ROUNDING_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_DATE_ALL_TESTS)) -def test_add_date(collection, test_case: ExpressionTestCase): - """Test $add date arithmetic: numeric types, operand position, and sign handling.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py deleted file mode 100644 index f5c817b3d..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_errors.py +++ /dev/null @@ -1,203 +0,0 @@ -from datetime import datetime, timezone - -import pytest -from bson import Binary, Code, MaxKey, MinKey, ObjectId, Regex, Timestamp - -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 - ExpressionTestCase, -) -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( - assert_expression_result, - execute_expression_with_insert, -) -from documentdb_tests.framework.error_codes import ( - MORE_THAN_ONE_DATE_ERROR, - OVERFLOW_ERROR, - TYPE_MISMATCH_ERROR, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import ( - DECIMAL128_INFINITY, - DECIMAL128_NAN, - FLOAT_INFINITY, - FLOAT_NAN, - INT64_MAX, -) - -# Property [Type Strictness]: $add rejects non-numeric, non-date operand types. -ADD_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - f"type_{tid}", - doc={"a": 1, "b": val}, - expression={"$add": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg=f"$add should reject a {tid} operand", - ) - for tid, val in [ - ("string", "string"), - ("bool", True), - ("array", [2, 3]), - ("object", {"a": 2}), - ("regex", Regex("abc")), - ("objectid", ObjectId("507f1f77bcf86cd799439011")), - ("binary", Binary(b"data")), - ("minkey", MinKey()), - ("maxkey", MaxKey()), - ("timestamp", Timestamp(1, 1)), - ("code", Code("function(){}")), - ] -] - -# Property [Mixed Valid and Invalid]: $add rejects an invalid operand when it appears among -# valid numeric operands. -ADD_MIXED_VALID_INVALID_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "mixed_valid_invalid", - doc={"a": 1, "b": 2, "c": "string"}, - expression={"$add": ["$a", "$b", "$c"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should error when a string appears among numeric operands", - ), -] - -# Property [Single Invalid Operand]: $add rejects a single operand of an invalid type. -ADD_SINGLE_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "single_string", - doc={"a": "string"}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single string operand", - ), - ExpressionTestCase( - "single_boolean", - doc={"a": True}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single boolean operand", - ), - ExpressionTestCase( - "single_array", - doc={"a": [1, 2]}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single array operand", - ), - ExpressionTestCase( - "single_object", - doc={"a": {"x": 1}}, - expression={"$add": ["$a"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$add should reject a single object operand", - ), -] - -# Property [Multiple Dates]: $add rejects expressions with more than one date operand. -ADD_MULTIPLE_DATE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "add_two_identical_dates", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 1, tzinfo=timezone.utc), - }, - expression={"$add": ["$a", "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when adding two identical date operands", - ), - ExpressionTestCase( - "two_different_dates", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 2, tzinfo=timezone.utc), - }, - expression={"$add": ["$a", "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when adding two different date operands", - ), - ExpressionTestCase( - "two_dates_with_numbers", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 2, tzinfo=timezone.utc), - }, - expression={"$add": [1, 2, 3, "$a", "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when two dates appear among numeric operands", - ), - ExpressionTestCase( - "dates_separated_by_number", - doc={ - "a": datetime(2026, 1, 1, tzinfo=timezone.utc), - "b": datetime(2026, 1, 2, tzinfo=timezone.utc), - }, - expression={"$add": ["$a", 1, "$b"]}, - error_code=MORE_THAN_ONE_DATE_ERROR, - msg="$add should error when two dates are separated by a numeric operand", - ), -] - -# Property [Date with Non-Finite]: $add rejects NaN and Infinity as numeric operands when a -# date is also present, since the resulting date would be non-representable. -ADD_DATE_NON_FINITE_ERROR_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_nan", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_NAN}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and float NaN", - ), - ExpressionTestCase( - "date_infinity", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_INFINITY}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and float infinity", - ), - ExpressionTestCase( - "date_decimal_nan", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_NAN}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and decimal128 NaN", - ), - ExpressionTestCase( - "date_decimal_infinity", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_INFINITY}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding a date and decimal128 infinity", - ), -] - -# Property [Date Overflow]: $add errors when the millisecond offset would push the date result -# beyond the representable date range. -ADD_DATE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "date_int64_max", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": INT64_MAX}, - expression={"$add": ["$a", "$b"]}, - error_code=OVERFLOW_ERROR, - msg="$add should error when adding INT64_MAX milliseconds to a date overflows the date range", # noqa: E501 - ), -] - -ADD_ERROR_ALL_TESTS = ( - ADD_TYPE_ERROR_TESTS - + ADD_MIXED_VALID_INVALID_TESTS - + ADD_SINGLE_TYPE_ERROR_TESTS - + ADD_MULTIPLE_DATE_TESTS - + ADD_DATE_NON_FINITE_ERROR_TESTS - + ADD_DATE_OVERFLOW_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_ERROR_ALL_TESTS)) -def test_add_errors(collection, test_case: ExpressionTestCase): - """Test $add type, multiple-date, and date non-finite error cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py deleted file mode 100644 index 7019c1b0b..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_input_forms.py +++ /dev/null @@ -1,47 +0,0 @@ -import pytest - -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 - ExpressionTestCase, -) -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( - assert_expression_result, - execute_expression_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Expression Input]: $add evaluates a nested expression argument before summing. -ADD_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "nested_add", - doc={"a": 1, "b": 2}, - expression={"$add": [{"$add": ["$a", "$b"]}, 3]}, - expected=6, - msg="$add should evaluate a nested $add expression as an operand", - ), -] - -# Property [Mixed Literal and Field]: $add accepts a mix of field references and inline literals -# in the same operand list. -ADD_MIXED_INPUT_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "mixed_literal_and_field", - doc={"a": 10}, - expression={"$add": ["$a", 5]}, - expected=15, - msg="$add should sum a field reference and an inline literal operand", - ), -] - -ADD_INPUT_FORM_ALL_TESTS = ADD_EXPRESSION_INPUT_TESTS + ADD_MIXED_INPUT_TESTS - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_INPUT_FORM_ALL_TESTS)) -def test_add_input_forms(collection, test_case: ExpressionTestCase): - """Test $add literal, nested expression, and mixed input form cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py deleted file mode 100644 index 0343c3316..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_non_finite.py +++ /dev/null @@ -1,154 +0,0 @@ -import math - -import pytest - -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 - ExpressionTestCase, -) -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( - assert_expression_result, - execute_expression_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import ( - DECIMAL128_INFINITY, - DECIMAL128_NAN, - DECIMAL128_NEGATIVE_INFINITY, - FLOAT_INFINITY, - FLOAT_NAN, - FLOAT_NEGATIVE_INFINITY, -) - -# Property [Infinity]: $add propagates infinity according to IEEE 754 rules. -ADD_INFINITY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "infinity", - doc={"a": FLOAT_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity when adding infinity and a finite number", - ), - ExpressionTestCase( - "negative_infinity", - doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity when adding negative infinity and a finite number", # noqa: E501 - ), - ExpressionTestCase( - "single_infinity", - doc={"a": FLOAT_INFINITY}, - expression={"$add": ["$a"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity for a single infinity operand", - ), - ExpressionTestCase( - "inf_plus_inf", - doc={"a": FLOAT_INFINITY, "b": FLOAT_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity when adding two positive infinities", - ), - ExpressionTestCase( - "neg_inf_plus_neg_inf", - doc={"a": FLOAT_NEGATIVE_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity when adding two negative infinities", - ), - ExpressionTestCase( - "inf_plus_zero", - doc={"a": FLOAT_INFINITY, "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return infinity when adding infinity and zero", - ), - ExpressionTestCase( - "neg_inf_plus_zero", - doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity when adding negative infinity and zero", - ), - ExpressionTestCase( - "decimal_infinity", - doc={"a": DECIMAL128_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_INFINITY, - msg="$add should return decimal128 infinity when adding decimal128 infinity and a number", - ), - ExpressionTestCase( - "decimal_negative_infinity", - doc={"a": DECIMAL128_NEGATIVE_INFINITY, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NEGATIVE_INFINITY, - msg="$add should return decimal128 negative infinity when adding decimal128 negative infinity and a number", # noqa: E501 - ), -] - -# Property [NaN]: $add propagates NaN according to IEEE 754 rules. -ADD_NAN_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "nan_add_one", - doc={"a": FLOAT_NAN, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding float NaN and a finite number", - ), - ExpressionTestCase( - "inf_minus_inf", - doc={"a": FLOAT_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding float infinity and negative infinity", - ), - ExpressionTestCase( - "nan_plus_nan", - doc={"a": FLOAT_NAN, "b": FLOAT_NAN}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding two float NaN values", - ), - ExpressionTestCase( - "nan_plus_inf", - doc={"a": FLOAT_NAN, "b": FLOAT_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(math.nan, nan_ok=True), - msg="$add should return NaN when adding float NaN and infinity", - ), - ExpressionTestCase( - "decimal_nan", - doc={"a": DECIMAL128_NAN, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NAN, - msg="$add should return decimal128 NaN when adding decimal128 NaN and a number", - ), - ExpressionTestCase( - "decimal_nan_plus_nan", - doc={"a": DECIMAL128_NAN, "b": DECIMAL128_NAN}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NAN, - msg="$add should return decimal128 NaN when adding two decimal128 NaN values", - ), - ExpressionTestCase( - "decimal_inf_minus_inf", - doc={"a": DECIMAL128_INFINITY, "b": DECIMAL128_NEGATIVE_INFINITY}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NAN, - msg="$add should return decimal128 NaN when adding decimal128 infinity and negative infinity", # noqa: E501 - ), -] - -ADD_NON_FINITE_ALL_TESTS = ADD_INFINITY_TESTS + ADD_NAN_TESTS - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_NON_FINITE_ALL_TESTS)) -def test_add_non_finite(collection, test_case: ExpressionTestCase): - """Test $add infinity and NaN propagation cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py deleted file mode 100644 index 9651d52d3..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_null.py +++ /dev/null @@ -1,86 +0,0 @@ -from datetime import datetime, timezone - -import pytest - -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 - ExpressionTestCase, -) -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( - assert_expression_result, - execute_expression_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import MISSING - -# Property [Null Propagation]: $add returns null if any operand is null or refers to a missing -# field. -ADD_NULL_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "single_null", - doc={"a": None}, - expression={"$add": ["$a"]}, - expected=None, - msg="$add should return null for a single null operand", - ), - ExpressionTestCase( - "null_operand", - doc={"a": 1, "b": None}, - expression={"$add": ["$a", "$b"]}, - expected=None, - msg="$add should return null when any operand is null", - ), - ExpressionTestCase( - "missing_field", - doc={}, - expression={"$add": [1, MISSING]}, - expected=None, - msg="$add should return null when any operand is a missing field", - ), - ExpressionTestCase( - "null_with_multiple", - doc={"a": 1, "b": 2, "c": None}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=None, - msg="$add should return null when null appears among multiple operands", - ), - ExpressionTestCase( - "null_in_middle", - doc={"a": 1, "b": 2, "c": 3, "e": 5}, - expression={"$add": ["$a", "$b", "$c", None, "$e"]}, - expected=None, - msg="$add should return null when null appears in the middle of operands", - ), - ExpressionTestCase( - "all_null", - doc={"a": None, "b": None}, - expression={"$add": ["$a", "$b"]}, - expected=None, - msg="$add should return null when all operands are null", - ), - ExpressionTestCase( - "all_missing", - doc={}, - expression={"$add": [MISSING, MISSING]}, - expected=None, - msg="$add should return null when all operands are missing fields", - ), - ExpressionTestCase( - "date_and_null", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": None}, - expression={"$add": ["$a", "$b"]}, - expected=None, - msg="$add should return null when a date is paired with a null operand", - ), -] - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_NULL_TESTS)) -def test_add_null(collection, test_case: ExpressionTestCase): - """Test $add null and missing field propagation cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py deleted file mode 100644 index 3c4d6f305..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_numeric.py +++ /dev/null @@ -1,259 +0,0 @@ -import pytest -from bson import Decimal128, Int64 - -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 - ExpressionTestCase, -) -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( - assert_expression_result, - execute_expression_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Same-Type Addition]: $add of two values of the same numeric type returns a value of -# that type. -ADD_SAME_TYPE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "same_type_int32", - doc={"a": 1, "b": 2}, - expression={"$add": ["$a", "$b"]}, - expected=3, - msg="$add should add two int32 values", - ), - ExpressionTestCase( - "same_type_int64", - doc={"a": Int64(10), "b": Int64(20)}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(30), - msg="$add should add two int64 values", - ), - ExpressionTestCase( - "same_type_double", - doc={"a": 1.5, "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=4.0, - msg="$add should add two double values", - ), - ExpressionTestCase( - "same_type_decimal", - doc={"a": Decimal128("10.5"), "b": Decimal128("20.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("31.0"), - msg="$add should add two decimal128 values", - ), -] - -# Property [Type Promotion]: $add promotes to the wider type when operands have different numeric -# types. Precedence: decimal128 > double > int64 > int32. -ADD_MIXED_TYPE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "int32_int64", - doc={"a": 1, "b": Int64(20)}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(21), - msg="$add should return int64 when adding int32 and int64", - ), - ExpressionTestCase( - "int32_double", - doc={"a": 1, "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=3.5, - msg="$add should return double when adding int32 and double", - ), - ExpressionTestCase( - "int32_decimal", - doc={"a": 1, "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("3.5"), - msg="$add should return decimal128 when adding int32 and decimal128", - ), - ExpressionTestCase( - "int64_double", - doc={"a": Int64(10), "b": 2.5}, - expression={"$add": ["$a", "$b"]}, - expected=12.5, - msg="$add should return double when adding int64 and double", - ), - ExpressionTestCase( - "int64_decimal", - doc={"a": Int64(10), "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("12.5"), - msg="$add should return decimal128 when adding int64 and decimal128", - ), - ExpressionTestCase( - "double_decimal", - doc={"a": 1.5, "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(Decimal128("4.00000000000000")), - msg="$add should return decimal128 when adding double and decimal128", - ), - ExpressionTestCase( - "three_mixed_types", - doc={"a": Decimal128("1.5"), "b": 2.5, "c": Int64(3)}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=pytest.approx(Decimal128("7.00000000000000")), - msg="$add should return decimal128 when adding decimal128, double, and int64", - ), -] - -# Property [Multiple Operands]: $add correctly sums three or more operands. -ADD_MULTIPLE_OPERANDS_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "multiple_int32", - doc={"a": 1, "b": 2, "c": 3}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=6, - msg="$add should add multiple int32 values", - ), - ExpressionTestCase( - "multiple_int64", - doc={"a": Int64(1), "b": Int64(2), "c": Int64(3)}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=Int64(6), - msg="$add should add multiple int64 values", - ), - ExpressionTestCase( - "multiple_double", - doc={"a": 1.1, "b": 2.2, "c": 3.3}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=pytest.approx(6.6), - msg="$add should add multiple double values", - ), - ExpressionTestCase( - "multiple_decimal", - doc={ - "a": Decimal128("1"), - "b": Decimal128("2"), - "c": Decimal128("3"), - "d": Decimal128("4"), - }, - expression={"$add": ["$a", "$b", "$c", "$d"]}, - expected=Decimal128("10"), - msg="$add should add multiple decimal128 values", - ), - ExpressionTestCase( - "five_operands", - doc={"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, - expression={"$add": ["$a", "$b", "$c", "$d", "$e"]}, - expected=15, - msg="$add should correctly sum five int32 operands", - ), - ExpressionTestCase( - "ten_operands", - doc={ - "a": 1, - "b": 2, - "c": 3, - "d": 4, - "e": 5, - "f": 6, - "g": 7, - "h": 8, - "i": 9, - "j": 10, - }, - expression={"$add": ["$a", "$b", "$c", "$d", "$e", "$f", "$g", "$h", "$i", "$j"]}, - expected=55, - msg="$add should correctly sum ten int32 operands", - ), -] - -# Property [Empty and Single Operand]: $add of zero operands returns 0; single operand returns -# that value unchanged. -ADD_SINGLE_AND_EMPTY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "empty", - doc={}, - expression={"$add": []}, - expected=0, - msg="$add should return 0 for empty operand list", - ), - ExpressionTestCase( - "single_int32", - doc={"a": 5}, - expression={"$add": ["$a"]}, - expected=5, - msg="$add should return the value for a single int32 operand", - ), - ExpressionTestCase( - "single_int64", - doc={"a": Int64(0)}, - expression={"$add": ["$a"]}, - expected=Int64(0), - msg="$add should return the value for a single int64 operand", - ), - ExpressionTestCase( - "single_double", - doc={"a": 0.0}, - expression={"$add": ["$a"]}, - expected=0.0, - msg="$add should return the value for a single double operand", - ), - ExpressionTestCase( - "single_decimal", - doc={"a": Decimal128("0")}, - expression={"$add": ["$a"]}, - expected=Decimal128("0"), - msg="$add should return the value for a single decimal128 operand", - ), -] - -# Property [Sign Handling]: $add handles negative values and zero correctly. -ADD_SIGN_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "negative_positive", - doc={"a": -5, "b": 3}, - expression={"$add": ["$a", "$b"]}, - expected=-2, - msg="$add should add a negative and a positive int32 value", - ), - ExpressionTestCase( - "both_negative", - doc={"a": -10, "b": -20}, - expression={"$add": ["$a", "$b"]}, - expected=-30, - msg="$add should add two negative int32 values", - ), - ExpressionTestCase( - "zeros", - doc={"a": 0, "b": 0}, - expression={"$add": ["$a", "$b"]}, - expected=0, - msg="$add should return 0 when adding two int32 zeros", - ), - ExpressionTestCase( - "zero_negative_zero", - doc={"a": 0, "b": -0.0}, - expression={"$add": ["$a", "$b"]}, - expected=0.0, - msg="$add should return 0.0 when adding int32 zero and negative zero double", - ), - ExpressionTestCase( - "sum_to_zero", - doc={"a": 1, "b": 0, "c": -1}, - expression={"$add": ["$a", "$b", "$c"]}, - expected=0, - msg="$add should return 0 when operands sum to zero", - ), -] - -ADD_NUMERIC_ALL_TESTS = ( - ADD_SAME_TYPE_TESTS - + ADD_MIXED_TYPE_TESTS - + ADD_MULTIPLE_OPERANDS_TESTS - + ADD_SINGLE_AND_EMPTY_TESTS - + ADD_SIGN_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_NUMERIC_ALL_TESTS)) -def test_add_numeric(collection, test_case: ExpressionTestCase): - """Test $add numeric type combinations, multiple operands, and sign handling.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py deleted file mode 100644 index 29a992905..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_overflow.py +++ /dev/null @@ -1,95 +0,0 @@ -import pytest -from bson import Int64 - -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 - ExpressionTestCase, -) -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( - assert_expression_result, - execute_expression_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import ( - DOUBLE_FROM_INT64_MAX, - FLOAT_INFINITY, - FLOAT_NEGATIVE_INFINITY, - INT32_MAX, - INT32_MIN, - INT32_OVERFLOW, - INT32_UNDERFLOW, - INT64_MAX, - INT64_MIN, -) - -# Property [Int32 Overflow]: when an int32 result exceeds the int32 range, $add promotes to -# int64. -ADD_INT32_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "int32_overflow", - doc={"a": INT32_MAX, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(INT32_OVERFLOW), - msg="$add should promote to int64 when the int32 result overflows INT32_MAX", - ), - ExpressionTestCase( - "int32_underflow", - doc={"a": INT32_MIN, "b": -1}, - expression={"$add": ["$a", "$b"]}, - expected=Int64(INT32_UNDERFLOW), - msg="$add should promote to int64 when the int32 result underflows INT32_MIN", - ), -] - -# Property [Int64 Overflow]: when an int64 result exceeds the int64 range, $add promotes to -# double. -ADD_INT64_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "int64_overflow", - doc={"a": INT64_MAX, "b": 1}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(DOUBLE_FROM_INT64_MAX), - msg="$add should promote to double when the int64 result overflows INT64_MAX", - ), - ExpressionTestCase( - "int64_underflow", - doc={"a": INT64_MIN, "b": -1}, - expression={"$add": ["$a", "$b"]}, - expected=pytest.approx(-DOUBLE_FROM_INT64_MAX), - msg="$add should promote to double when the int64 result underflows INT64_MIN", - ), -] - -# Property [Double Overflow]: when a double result exceeds the double range, $add returns -# infinity. -ADD_DOUBLE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "double_overflow", - doc={"a": 1e308, "b": 1e308}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_INFINITY, - msg="$add should return positive infinity on double overflow", - ), - ExpressionTestCase( - "double_underflow", - doc={"a": -1e308, "b": -1e308}, - expression={"$add": ["$a", "$b"]}, - expected=FLOAT_NEGATIVE_INFINITY, - msg="$add should return negative infinity on double underflow", - ), -] - -ADD_OVERFLOW_ALL_TESTS = ( - ADD_INT32_OVERFLOW_TESTS + ADD_INT64_OVERFLOW_TESTS + ADD_DOUBLE_OVERFLOW_TESTS -) - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_OVERFLOW_ALL_TESTS)) -def test_add_overflow(collection, test_case: ExpressionTestCase): - """Test $add integer and double overflow and underflow cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py deleted file mode 100644 index f0cdd34ab..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_precision.py +++ /dev/null @@ -1,103 +0,0 @@ -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_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import ( - DECIMAL128_INFINITY, - DECIMAL128_MAX, - DECIMAL128_MIN, - DECIMAL128_NEGATIVE_INFINITY, -) - -# Property [Decimal128 Precision]: $add preserves decimal128 precision, including exact -# representation of values that are inexact in double. -ADD_DECIMAL_PRECISION_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "decimal_precision", - doc={"a": Decimal128("1.5"), "b": Decimal128("2.5")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("4.0"), - msg="$add should preserve decimal128 precision", - ), - ExpressionTestCase( - "decimal_precision_small", - doc={"a": Decimal128("0.1"), "b": Decimal128("0.2")}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("0.3"), - msg="$add should exactly represent 0.1 + 0.2 with decimal128", - ), - ExpressionTestCase( - "decimal_large_precision", - doc={ - "a": Decimal128("999999999999999999999999999999999"), - "b": Decimal128("1"), - }, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("1000000000000000000000000000000000"), - msg="$add should handle large decimal128 addition with full precision", - ), - ExpressionTestCase( - "decimal_large_negative_precision", - doc={ - "a": Decimal128("-999999999999999999999999999999999"), - "b": Decimal128("-1"), - }, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("-1000000000000000000000000000000000"), - msg="$add should handle large negative decimal128 addition with full precision", - ), -] - -# Property [Decimal128 Boundaries]: $add at decimal128 boundary values promotes to infinity when -# the result overflows, and returns zero when max and min cancel. -ADD_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "decimal128_max_plus_zero", - doc={"a": DECIMAL128_MAX, "b": Decimal128("0")}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_MAX, - msg="$add should return decimal128 max when adding zero to decimal128 max", - ), - ExpressionTestCase( - "decimal128_max_plus_max", - doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MAX}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_INFINITY, - msg="$add should return decimal128 infinity when adding two decimal128 max values", - ), - ExpressionTestCase( - "decimal128_min_plus_min", - doc={"a": DECIMAL128_MIN, "b": DECIMAL128_MIN}, - expression={"$add": ["$a", "$b"]}, - expected=DECIMAL128_NEGATIVE_INFINITY, - msg="$add should return decimal128 negative infinity when adding two decimal128 min values", - ), - ExpressionTestCase( - "decimal128_max_plus_min", - doc={"a": DECIMAL128_MAX, "b": DECIMAL128_MIN}, - expression={"$add": ["$a", "$b"]}, - expected=Decimal128("0E+6111"), - msg="$add should return zero when adding decimal128 max and decimal128 min", - ), -] - -ADD_PRECISION_ALL_TESTS = ADD_DECIMAL_PRECISION_TESTS + ADD_DECIMAL_BOUNDARY_TESTS - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_PRECISION_ALL_TESTS)) -def test_add_precision(collection, test_case: ExpressionTestCase): - """Test $add decimal128 precision and boundary value cases.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py deleted file mode 100644 index 7af9e0b66..000000000 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/test_add_return_type.py +++ /dev/null @@ -1,114 +0,0 @@ -from datetime import datetime, timezone - -import pytest -from bson import Decimal128, Int64 - -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 - ExpressionTestCase, -) -from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( - assert_expression_result, - execute_expression_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Return Type]: $add follows numeric type promotion rules to determine the result type. -# Precedence: decimal128 > double > int64 > int32. Date + numeric always returns date. -ADD_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "return_type_int_int", - doc={"a": 1, "b": 2}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="int", - msg="$add should return int type when adding two int32 values", - ), - ExpressionTestCase( - "return_type_int_long", - doc={"a": 1, "b": Int64(2)}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="long", - msg="$add should return long type when adding int32 and int64", - ), - ExpressionTestCase( - "return_type_int_double", - doc={"a": 1, "b": 2.0}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="double", - msg="$add should return double type when adding int32 and double", - ), - ExpressionTestCase( - "return_type_int_decimal", - doc={"a": 1, "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding int32 and decimal128", - ), - ExpressionTestCase( - "return_type_long_long", - doc={"a": Int64(1), "b": Int64(2)}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="long", - msg="$add should return long type when adding two int64 values", - ), - ExpressionTestCase( - "return_type_long_double", - doc={"a": Int64(1), "b": 2.0}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="double", - msg="$add should return double type when adding int64 and double", - ), - ExpressionTestCase( - "return_type_long_decimal", - doc={"a": Int64(1), "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding int64 and decimal128", - ), - ExpressionTestCase( - "return_type_double_double", - doc={"a": 1.0, "b": 2.0}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="double", - msg="$add should return double type when adding two double values", - ), - ExpressionTestCase( - "return_type_double_decimal", - doc={"a": 1.0, "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding double and decimal128", - ), - ExpressionTestCase( - "return_type_decimal_decimal", - doc={"a": Decimal128("1"), "b": Decimal128("2")}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="decimal", - msg="$add should return decimal type when adding two decimal128 values", - ), - ExpressionTestCase( - "return_type_date_int", - doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": 1000}, - expression={"$type": {"$add": ["$a", "$b"]}}, - expected="date", - msg="$add should return date type when adding a date and an int32", - ), - ExpressionTestCase( - "return_type_empty", - doc={}, - expression={"$type": {"$add": []}}, - expected="int", - msg="$add should return int type for an empty operand list", - ), -] - - -@pytest.mark.parametrize("test_case", pytest_params(ADD_RETURN_TYPE_TESTS)) -def test_add_return_type(collection, test_case: ExpressionTestCase): - """Test $add return type promotion rules for all numeric type combinations.""" - result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) - assert_expression_result( - result, - expected=test_case.expected, - error_code=test_case.error_code, - msg=test_case.msg, - ) From 3be56ce8ea15a62039db48fb08c6768792beb552 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 10 Jul 2026 09:26:00 -0700 Subject: [PATCH 05/10] Add subtract tests Signed-off-by: PatersonProjects --- .../subtract/test_subtract_basic.py | 178 ++++++++ .../subtract/test_subtract_boundaries.py | 405 ++++++++++++++++++ .../subtract/test_subtract_bson_types.py | 67 +++ .../subtract/test_subtract_dates.py | 138 ++++++ .../subtract/test_subtract_errors.py | 277 ++++++++++++ .../subtract/test_subtract_input_forms.py | 90 ++++ .../subtract/test_subtract_non_finite.py | 178 ++++++++ .../arithmetic/subtract/test_subtract_null.py | 80 ++++ 8 files changed, 1413 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_bson_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py new file mode 100644 index 000000000..350949932 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py @@ -0,0 +1,178 @@ +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.aggregate + +# Property [Same-type arithmetic]: $subtract preserves the BSON type when both operands share +# a type. +# Property [Mixed-type promotion]: $subtract promotes the result to the wider of the two input +# types. +# Property [Sign handling]: $subtract correctly computes the sign of the result. +# Property [Zero handling]: $subtract handles zero operands correctly. +SUBTRACT_BASIC_TESTS: list[ExpressionTestCase] = [ + # Same type operations + ExpressionTestCase( + "same_type_int32", + doc={"a": 10, "b": 3}, + expression={"$subtract": ["$a", "$b"]}, + expected=7, + msg="$subtract should return int32 for int32 - int32", + ), + ExpressionTestCase( + "same_type_int64", + doc={"a": Int64(20), "b": Int64(5)}, + expression={"$subtract": ["$a", "$b"]}, + expected=Int64(15), + msg="$subtract should return int64 for int64 - int64", + ), + ExpressionTestCase( + "same_type_double", + doc={"a": 10.5, "b": 2.5}, + expression={"$subtract": ["$a", "$b"]}, + expected=8.0, + msg="$subtract should return double for double - double", + ), + ExpressionTestCase( + "same_type_decimal", + doc={"a": Decimal128("20.5"), "b": Decimal128("10.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("10.0"), + msg="$subtract should return Decimal128 for Decimal128 - Decimal128", + ), + # Mixed numeric type promotion + ExpressionTestCase( + "int32_int64", + doc={"a": 10, "b": Int64(3)}, + expression={"$subtract": ["$a", "$b"]}, + expected=Int64(7), + msg="$subtract should promote to int64 for int32 - int64", + ), + ExpressionTestCase( + "int32_double", + doc={"a": 10, "b": 2.5}, + expression={"$subtract": ["$a", "$b"]}, + expected=7.5, + msg="$subtract should promote to double for int32 - double", + ), + ExpressionTestCase( + "int32_decimal", + doc={"a": 10, "b": Decimal128("2.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("7.5"), + msg="$subtract should promote to Decimal128 for int32 - Decimal128", + ), + ExpressionTestCase( + "int64_double", + doc={"a": Int64(20), "b": 5.5}, + expression={"$subtract": ["$a", "$b"]}, + expected=14.5, + msg="$subtract should promote to double for int64 - double", + ), + ExpressionTestCase( + "int64_decimal", + doc={"a": Int64(20), "b": Decimal128("5.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("14.5"), + msg="$subtract should promote to Decimal128 for int64 - Decimal128", + ), + ExpressionTestCase( + "double_decimal", + doc={"a": 10.5, "b": Decimal128("2.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("8.0000000000000"), + msg="$subtract should promote to Decimal128 for double - Decimal128", + ), + # Sign handling + ExpressionTestCase( + "negative_positive", + doc={"a": -10, "b": 3}, + expression={"$subtract": ["$a", "$b"]}, + expected=-13, + msg="$subtract should return negative for negative minuend minus positive subtrahend", + ), + ExpressionTestCase( + "positive_negative", + doc={"a": 10, "b": -3}, + expression={"$subtract": ["$a", "$b"]}, + expected=13, + msg="$subtract should return positive for positive minuend minus negative subtrahend", + ), + ExpressionTestCase( + "both_negative", + doc={"a": -10, "b": -3}, + expression={"$subtract": ["$a", "$b"]}, + expected=-7, + msg="$subtract should return negative when both operands are negative and |a| > |b|", + ), + ExpressionTestCase( + "result_negative", + doc={"a": 5, "b": 10}, + expression={"$subtract": ["$a", "$b"]}, + expected=-5, + msg="$subtract should return negative when the minuend is smaller than the subtrahend", + ), + ExpressionTestCase( + "result_negative_double", + doc={"a": 5.5, "b": 10}, + expression={"$subtract": ["$a", "$b"]}, + expected=-4.5, + msg="$subtract should return negative double when minuend is smaller than subtrahend", + ), + # Zero handling + ExpressionTestCase( + "zero_minuend", + doc={"a": 0, "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + expected=-5, + msg="$subtract should return negated subtrahend when the minuend is zero", + ), + ExpressionTestCase( + "zero_subtrahend", + doc={"a": 5, "b": 0}, + expression={"$subtract": ["$a", "$b"]}, + expected=5, + msg="$subtract should return the minuend unchanged when the subtrahend is zero", + ), + ExpressionTestCase( + "zeros", + doc={"a": 0, "b": 0}, + expression={"$subtract": ["$a", "$b"]}, + expected=0, + msg="$subtract should return zero for zero - zero", + ), + ExpressionTestCase( + "zero_negative_zero", + doc={"a": 0, "b": -0.0}, + expression={"$subtract": ["$a", "$b"]}, + expected=0.0, + msg="$subtract of int32 zero minus double negative-zero should return double positive-zero", + ), + ExpressionTestCase( + "negative_zero_zero", + doc={"a": -0.0, "b": 0}, + expression={"$subtract": ["$a", "$b"]}, + expected=-0.0, + msg="$subtract of double negative-zero minus int32 zero should return double negative-zero", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_BASIC_TESTS)) +def test_subtract_basic(collection, test_case: ExpressionTestCase): + """Test $subtract same-type and mixed-type numeric arithmetic.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py new file mode 100644 index 000000000..180db3508 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py @@ -0,0 +1,405 @@ +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_HALF, + DECIMAL128_JUST_ABOVE_HALF, + DECIMAL128_JUST_BELOW_HALF, + DECIMAL128_LARGE_EXPONENT, + DECIMAL128_MANY_TRAILING_ZEROS, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_NEGATIVE_HALF, + 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_MAX_SAFE_INTEGER, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + DOUBLE_NEGATIVE_HALF, + DOUBLE_NEGATIVE_ONE_AND_HALF, + DOUBLE_ONE_AND_HALF, + DOUBLE_TWO_AND_HALF, + FLOAT_INFINITY, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MAX_MINUS_1, + INT32_MIN, + INT32_MIN_PLUS_1, + INT32_OVERFLOW, + INT32_UNDERFLOW, + INT64_MAX, + INT64_MAX_MINUS_1, + INT64_MIN, + INT64_MIN_PLUS_1, +) + +pytestmark = pytest.mark.aggregate + +# Property [Int32 overflow]: $subtract promotes int32 results to int64 on overflow/underflow. +# Property [Int64 overflow]: $subtract promotes int64 results to double on overflow/underflow. +# Property [Double overflow]: $subtract returns ±Infinity on double overflow. +# Property [Decimal128 precision]: $subtract preserves Decimal128 full precision. +SUBTRACT_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + # Int32 overflow and underflow + ExpressionTestCase( + "int32_overflow", + doc={"a": INT32_MAX, "b": -1}, + expression={"$subtract": ["$a", "$b"]}, + expected=INT32_OVERFLOW, + msg="$subtract should promote to int64 when the int32 result exceeds INT32_MAX", + ), + ExpressionTestCase( + "int32_underflow", + doc={"a": INT32_MIN, "b": 1}, + expression={"$subtract": ["$a", "$b"]}, + expected=INT32_UNDERFLOW, + msg="$subtract should promote to int64 when the int32 result is below INT32_MIN", + ), + # Int32 boundary values + ExpressionTestCase( + "int32_max_minuend", + doc={"a": INT32_MAX, "b": 10}, + expression={"$subtract": ["$a", "$b"]}, + expected=2147483637, + msg="$subtract should correctly subtract from INT32_MAX", + ), + ExpressionTestCase( + "int32_max_minus_1_minuend", + doc={"a": INT32_MAX_MINUS_1, "b": 10}, + expression={"$subtract": ["$a", "$b"]}, + expected=2147483636, + msg="$subtract should correctly subtract from INT32_MAX - 1", + ), + ExpressionTestCase( + "int32_min_minuend", + doc={"a": INT32_MIN, "b": 10}, + expression={"$subtract": ["$a", "$b"]}, + expected=-2147483658, + msg="$subtract should promote to int64 when subtracting from INT32_MIN", + ), + ExpressionTestCase( + "int32_min_plus_1_minuend", + doc={"a": INT32_MIN_PLUS_1, "b": 10}, + expression={"$subtract": ["$a", "$b"]}, + expected=-2147483657, + msg="$subtract should promote to int64 when subtracting from INT32_MIN + 1", + ), + ExpressionTestCase( + "int32_max_subtrahend", + doc={"a": 10, "b": INT32_MAX}, + expression={"$subtract": ["$a", "$b"]}, + expected=-2147483637, + msg="$subtract should correctly subtract INT32_MAX as the subtrahend", + ), + ExpressionTestCase( + "int32_min_subtrahend", + doc={"a": 10, "b": INT32_MIN}, + expression={"$subtract": ["$a", "$b"]}, + expected=2147483658, + msg="$subtract should promote to int64 when INT32_MIN is the subtrahend", + ), + # Int64 overflow and underflow + ExpressionTestCase( + "int64_overflow", + doc={"a": INT64_MAX, "b": -1}, + expression={"$subtract": ["$a", "$b"]}, + expected=9.223372036854776e18, + msg="$subtract should promote to double when the int64 result exceeds INT64_MAX", + ), + ExpressionTestCase( + "int64_underflow", + doc={"a": INT64_MIN, "b": 1}, + expression={"$subtract": ["$a", "$b"]}, + expected=-9.223372036854776e18, + msg="$subtract should promote to double when the int64 result is below INT64_MIN", + ), + # Int64 boundary values + ExpressionTestCase( + "int64_max_minuend", + doc={"a": INT64_MAX, "b": Int64(10)}, + expression={"$subtract": ["$a", "$b"]}, + expected=Int64(9223372036854775797), + msg="$subtract should correctly subtract from INT64_MAX", + ), + ExpressionTestCase( + "int64_max_minus_1_minuend", + doc={"a": INT64_MAX_MINUS_1, "b": Int64(10)}, + expression={"$subtract": ["$a", "$b"]}, + expected=Int64(9223372036854775796), + msg="$subtract should correctly subtract from INT64_MAX - 1", + ), + ExpressionTestCase( + "int64_min_minuend", + doc={"a": INT64_MIN, "b": Int64(10)}, + expression={"$subtract": ["$a", "$b"]}, + expected=-9.223372036854776e18, + msg="$subtract should overflow to double when subtracting from INT64_MIN", + ), + ExpressionTestCase( + "int64_min_plus_1_minuend", + doc={"a": INT64_MIN_PLUS_1, "b": Int64(10)}, + expression={"$subtract": ["$a", "$b"]}, + expected=-9.223372036854776e18, + msg="$subtract should overflow to double when subtracting from INT64_MIN + 1", + ), + ExpressionTestCase( + "int64_max_subtrahend", + doc={"a": Int64(10), "b": INT64_MAX}, + expression={"$subtract": ["$a", "$b"]}, + expected=Int64(-9223372036854775797), + msg="$subtract should correctly subtract INT64_MAX as the subtrahend", + ), + ExpressionTestCase( + "int64_min_subtrahend", + doc={"a": Int64(10), "b": INT64_MIN}, + expression={"$subtract": ["$a", "$b"]}, + expected=9.223372036854776e18, + msg="$subtract should overflow to double when INT64_MIN is the subtrahend", + ), + # Double overflow + ExpressionTestCase( + "double_overflow", + doc={"a": 1e308, "b": -1e308}, + expression={"$subtract": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$subtract should return Infinity on double overflow", + ), + ExpressionTestCase( + "double_underflow", + doc={"a": -1e308, "b": 1e308}, + expression={"$subtract": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$subtract should return -Infinity on double underflow", + ), + # Double boundary values + ExpressionTestCase( + "double_min_subnormal_minuend", + doc={"a": DOUBLE_MIN_SUBNORMAL, "b": 0}, + expression={"$subtract": ["$a", "$b"]}, + expected=5e-324, + msg="$subtract should handle the minimum subnormal double value", + ), + ExpressionTestCase( + "double_near_min_minuend", + doc={"a": DOUBLE_NEAR_MIN, "b": 0}, + expression={"$subtract": ["$a", "$b"]}, + expected=1e-308, + msg="$subtract should handle doubles near the minimum normal value", + ), + ExpressionTestCase( + "double_near_max_minuend", + doc={"a": DOUBLE_NEAR_MAX, "b": 1e307}, + expression={"$subtract": ["$a", "$b"]}, + expected=9e307, + msg="$subtract should handle doubles near the maximum value", + ), + ExpressionTestCase( + "double_max_safe_integer_minuend", + doc={"a": DOUBLE_MAX_SAFE_INTEGER, "b": 1}, + expression={"$subtract": ["$a", "$b"]}, + expected=9007199254740991.0, + msg="$subtract should handle the maximum safe integer double as the minuend", + ), + ExpressionTestCase( + "double_max_safe_integer_subtrahend", + doc={"a": 1, "b": DOUBLE_MAX_SAFE_INTEGER}, + expression={"$subtract": ["$a", "$b"]}, + expected=-9007199254740991.0, + msg="$subtract should handle the maximum safe integer double as the subtrahend", + ), + # Decimal128 boundary values + ExpressionTestCase( + "decimal128_max_minuend", + doc={"a": DECIMAL128_MAX, "b": Decimal128("1")}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_MAX, + msg="$subtract should handle DECIMAL128_MAX as the minuend", + ), + ExpressionTestCase( + "decimal128_min_minuend", + doc={"a": DECIMAL128_MIN, "b": Decimal128("1")}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_MIN, + msg="$subtract should handle DECIMAL128_MIN as the minuend", + ), + ExpressionTestCase( + "decimal128_small_exponent_minuend", + doc={"a": DECIMAL128_SMALL_EXPONENT, "b": Decimal128("0")}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_SMALL_EXPONENT, + msg="$subtract should handle Decimal128 with a small exponent", + ), + ExpressionTestCase( + "decimal128_large_exponent_minuend", + doc={"a": DECIMAL128_LARGE_EXPONENT, "b": Decimal128("1")}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_LARGE_EXPONENT, + msg="$subtract should handle Decimal128 with a large exponent", + ), + ExpressionTestCase( + "decimal128_trailing_zero", + doc={"a": DECIMAL128_TRAILING_ZERO, "b": Decimal128("0.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("0.5"), + msg="$subtract should handle Decimal128 with a trailing zero", + ), + ExpressionTestCase( + "decimal128_many_trailing_zeros", + doc={"a": DECIMAL128_MANY_TRAILING_ZEROS, "b": Decimal128("0.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("0.50000000000000000000000000000000"), + msg="$subtract should handle Decimal128 with many trailing zeros", + ), + # Decimal128 precision + ExpressionTestCase( + "decimal_precision", + doc={"a": Decimal128("10.5"), "b": Decimal128("2.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("8.0"), + msg="$subtract should preserve Decimal128 precision", + ), + ExpressionTestCase( + "decimal_precision_small", + doc={"a": Decimal128("0.3"), "b": Decimal128("0.1")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("0.2"), + msg="$subtract should compute 0.3 - 0.1 exactly in Decimal128 without floating-point error", + ), + ExpressionTestCase( + "decimal_large_precision", + doc={ + "a": Decimal128("1000000000000000000000000000000000"), + "b": Decimal128("1"), + }, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("999999999999999999999999999999999"), + msg="$subtract should handle large Decimal128 values with full precision", + ), + # Double rounding edge cases + ExpressionTestCase( + "double_half_minuend", + doc={"a": DOUBLE_HALF, "b": 0.25}, + expression={"$subtract": ["$a", "$b"]}, + expected=0.25, + msg="$subtract should correctly compute 0.5 - 0.25 = 0.25", + ), + ExpressionTestCase( + "double_one_and_half_minuend", + doc={"a": DOUBLE_ONE_AND_HALF, "b": 0.5}, + expression={"$subtract": ["$a", "$b"]}, + expected=1.0, + msg="$subtract should correctly compute 1.5 - 0.5 = 1.0", + ), + ExpressionTestCase( + "double_two_and_half_minuend", + doc={"a": DOUBLE_TWO_AND_HALF, "b": 1.5}, + expression={"$subtract": ["$a", "$b"]}, + expected=1.0, + msg="$subtract should correctly compute 2.5 - 1.5 = 1.0", + ), + ExpressionTestCase( + "double_negative_half_minuend", + doc={"a": DOUBLE_NEGATIVE_HALF, "b": 0.5}, + expression={"$subtract": ["$a", "$b"]}, + expected=-1.0, + msg="$subtract should correctly compute -0.5 - 0.5 = -1.0", + ), + ExpressionTestCase( + "double_negative_one_and_half_minuend", + doc={"a": DOUBLE_NEGATIVE_ONE_AND_HALF, "b": 0.5}, + expression={"$subtract": ["$a", "$b"]}, + expected=-2.0, + msg="$subtract should correctly compute -1.5 - 0.5 = -2.0", + ), + ExpressionTestCase( + "double_just_below_half_minuend", + doc={"a": DOUBLE_JUST_BELOW_HALF, "b": 0.25}, + expression={"$subtract": ["$a", "$b"]}, + expected=pytest.approx(0.2499999999999994), + msg="$subtract should handle a double just below 0.5", + ), + ExpressionTestCase( + "double_just_above_half_minuend", + doc={"a": DOUBLE_JUST_ABOVE_HALF, "b": 0.25}, + expression={"$subtract": ["$a", "$b"]}, + expected=pytest.approx(0.250000001), + msg="$subtract should handle a double just above 0.5", + ), + # Decimal128 rounding edge cases + ExpressionTestCase( + "decimal_half_minuend", + doc={"a": DECIMAL128_HALF, "b": Decimal128("0.25")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("0.25"), + msg="$subtract should correctly compute Decimal128 0.5 - 0.25 = 0.25", + ), + ExpressionTestCase( + "decimal_one_and_half_minuend", + doc={"a": DECIMAL128_ONE_AND_HALF, "b": Decimal128("0.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("1.0"), + msg="$subtract should correctly compute Decimal128 1.5 - 0.5 = 1.0", + ), + ExpressionTestCase( + "decimal_two_and_half_minuend", + doc={"a": DECIMAL128_TWO_AND_HALF, "b": Decimal128("1.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("1.0"), + msg="$subtract should correctly compute Decimal128 2.5 - 1.5 = 1.0", + ), + ExpressionTestCase( + "decimal_negative_half_minuend", + doc={"a": DECIMAL128_NEGATIVE_HALF, "b": Decimal128("0.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("-1.0"), + msg="$subtract should correctly compute Decimal128 -0.5 - 0.5 = -1.0", + ), + ExpressionTestCase( + "decimal_negative_one_and_half_minuend", + doc={"a": DECIMAL128_NEGATIVE_ONE_AND_HALF, "b": Decimal128("0.5")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("-2.0"), + msg="$subtract should correctly compute Decimal128 -1.5 - 0.5 = -2.0", + ), + ExpressionTestCase( + "decimal_just_below_half_minuend", + doc={"a": DECIMAL128_JUST_BELOW_HALF, "b": Decimal128("0.25")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("0.2499999999999999999999999999999999"), + msg="$subtract should handle Decimal128 just below 0.5", + ), + ExpressionTestCase( + "decimal_just_above_half_minuend", + doc={"a": DECIMAL128_JUST_ABOVE_HALF, "b": Decimal128("0.25")}, + expression={"$subtract": ["$a", "$b"]}, + expected=Decimal128("0.2500000000000000000000000000000001"), + msg="$subtract should handle Decimal128 just above 0.5", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_BOUNDARY_TESTS)) +def test_subtract_boundaries(collection, test_case: ExpressionTestCase): + """Test $subtract boundary values, overflow behavior, and numeric precision.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_bson_types.py new file mode 100644 index 000000000..b2f982d35 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_bson_types.py @@ -0,0 +1,67 @@ +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, +) + +pytestmark = pytest.mark.aggregate + +_NUMERIC_TYPES = [BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL] + +# Property [Minuend type acceptance]: $subtract accepts numeric and date types as minuend. +# Property [Minuend type rejection]: $subtract rejects all other BSON types as minuend. +SUBTRACT_MINUEND_SPEC = BsonTypeTestCase( + id="subtract_minuend", + msg="$subtract minuend type", + valid_types=_NUMERIC_TYPES + [BsonType.DATE, BsonType.NULL], +) + +# Property [Subtrahend type acceptance]: $subtract accepts numeric types as subtrahend. +# Property [Subtrahend type rejection]: $subtract rejects all other BSON types as subtrahend, +# including Date when the minuend is numeric. +SUBTRACT_SUBTRAHEND_SPEC = BsonTypeTestCase( + id="subtract_subtrahend", + msg="$subtract subtrahend type", + valid_types=_NUMERIC_TYPES + [BsonType.NULL], +) + +MINUEND_REJECTION_CASES = generate_bson_rejection_test_cases([SUBTRACT_MINUEND_SPEC]) +MINUEND_ACCEPTANCE_CASES = generate_bson_acceptance_test_cases([SUBTRACT_MINUEND_SPEC]) +SUBTRAHEND_REJECTION_CASES = generate_bson_rejection_test_cases([SUBTRACT_SUBTRAHEND_SPEC]) +SUBTRAHEND_ACCEPTANCE_CASES = generate_bson_acceptance_test_cases([SUBTRACT_SUBTRAHEND_SPEC]) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", MINUEND_REJECTION_CASES) +def test_subtract_rejects_invalid_minuend(collection, bson_type, sample_value, spec): + """Test $subtract rejects invalid BSON types as the minuend.""" + result = execute_expression_with_insert(collection, {"$subtract": [sample_value, 1]}, {}) + assert_expression_result(result, error_code=spec.expected_code(bson_type)) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", MINUEND_ACCEPTANCE_CASES) +def test_subtract_accepts_valid_minuend(collection, bson_type, sample_value, spec): + """Test $subtract accepts valid BSON types (numeric and date) as the minuend.""" + result = execute_expression_with_insert(collection, {"$subtract": [sample_value, 1]}, {}) + assertNotError(result, msg=f"{spec.msg} should accept {bson_type.value}") + + +@pytest.mark.parametrize("bson_type,sample_value,spec", SUBTRAHEND_REJECTION_CASES) +def test_subtract_rejects_invalid_subtrahend(collection, bson_type, sample_value, spec): + """Test $subtract rejects invalid BSON types as the subtrahend.""" + result = execute_expression_with_insert(collection, {"$subtract": [10, sample_value]}, {}) + assert_expression_result(result, error_code=spec.expected_code(bson_type)) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", SUBTRAHEND_ACCEPTANCE_CASES) +def test_subtract_accepts_valid_subtrahend(collection, bson_type, sample_value, spec): + """Test $subtract accepts valid BSON types (numeric) as the subtrahend.""" + result = execute_expression_with_insert(collection, {"$subtract": [10, sample_value]}, {}) + assertNotError(result, msg=f"{spec.msg} should accept {bson_type.value}") diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py new file mode 100644 index 000000000..657d49005 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py @@ -0,0 +1,138 @@ +from datetime import datetime, timezone + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.aggregate + +# Property [Date - numeric]: $subtract returns a date when the minuend is a date and subtrahend +# is numeric (milliseconds). +# Property [Date - date]: $subtract returns the difference in milliseconds when both operands +# are dates. +# Property [Date rounding]: fractional ms in the subtrahend are rounded using round-half-to-even. +SUBTRACT_DATE_TESTS: list[ExpressionTestCase] = [ + # Date minus numeric types + ExpressionTestCase( + "date_int32", + doc={ + "a": datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + "b": 86400000, + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="$subtract should subtract int32 milliseconds from a date", + ), + ExpressionTestCase( + "date_int64", + doc={ + "a": datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + "b": Int64(86400000), + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="$subtract should subtract int64 milliseconds from a date", + ), + ExpressionTestCase( + "date_decimal", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 1, tzinfo=timezone.utc), + "b": Decimal128("1.5"), + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 998000, tzinfo=timezone.utc), + msg="$subtract should round Decimal128 1.5 ms to 2 ms before subtracting from a date", + ), + ExpressionTestCase( + "date_double_round_up", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 0, 3000, tzinfo=timezone.utc), + "b": 2.5, + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="$subtract should round double 2.5 ms to 3 ms (round-half-to-even) before subtracting", + ), + ExpressionTestCase( + "date_double_truncates", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 0, 5000, tzinfo=timezone.utc), + "b": 4.4, + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), + msg="$subtract should round double 4.4 ms to 4 ms before subtracting from a date", + ), + # Date minus negative number (moves date forward) + ExpressionTestCase( + "date_negative", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "b": -86400000, + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + msg="$subtract should move a date forward when subtracting a negative millisecond offset", + ), + ExpressionTestCase( + "date_zero", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "b": 0, + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="$subtract should return the same date when subtracting zero milliseconds", + ), + ExpressionTestCase( + "date_negative_zero", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "b": -0.0, + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="$subtract should return the same date when subtracting negative-zero milliseconds", + ), + # Date minus date returns milliseconds + ExpressionTestCase( + "two_dates", + doc={ + "a": datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + "b": datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + }, + expression={"$subtract": ["$a", "$b"]}, + expected=86400000, + msg="$subtract of two dates should return the difference in milliseconds", + ), + ExpressionTestCase( + "two_dates_negative", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "b": datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc), + }, + expression={"$subtract": ["$a", "$b"]}, + expected=-86400000, + msg="$subtract of two dates should return negative ms when the minuend date is earlier", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_DATE_TESTS)) +def test_subtract_dates(collection, test_case: ExpressionTestCase): + """Test $subtract date arithmetic: date minus numeric and date minus date.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py new file mode 100644 index 000000000..9d2414632 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py @@ -0,0 +1,277 @@ +import uuid +from datetime import datetime, timezone + +import pytest +from bson import Binary, MaxKey, MinKey, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import ( + EXPRESSION_TYPE_MISMATCH_ERROR, + TYPE_MISMATCH_DATE_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_NAN, + FLOAT_INFINITY, + FLOAT_NAN, +) + +pytestmark = pytest.mark.aggregate + +_FIXED_UUID = uuid.UUID("550e8400-e29b-41d4-a716-446655440000") + +# Property [Type rejection]: $subtract rejects non-numeric, non-date types with TYPE_MISMATCH_ERROR. +# Property [Date constraint]: $subtract enforces date arithmetic type rules. +# Property [Date NaN/Inf]: $subtract rejects NaN/Infinity as a date offset with +# TYPE_MISMATCH_DATE_ERROR. +# Property [Arity]: $subtract requires exactly two arguments. +SUBTRACT_ERROR_TESTS: list[ExpressionTestCase] = [ + # Invalid minuend types + ExpressionTestCase( + "string_minuend", + doc={"a": "string", "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a string minuend", + ), + ExpressionTestCase( + "boolean_minuend", + doc={"a": True, "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a boolean minuend", + ), + ExpressionTestCase( + "array_minuend", + doc={"a": [2, 3], "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject an array minuend", + ), + ExpressionTestCase( + "object_minuend", + doc={"a": {"x": 2}, "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject an object minuend", + ), + ExpressionTestCase( + "empty_array_minuend", + doc={"a": [], "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject an empty array minuend", + ), + ExpressionTestCase( + "empty_object_minuend", + doc={"a": {}, "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject an empty object minuend", + ), + ExpressionTestCase( + "binary_minuend", + doc={"a": Binary(b"test", 0), "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a binary minuend", + ), + ExpressionTestCase( + "timestamp_minuend", + doc={"a": Timestamp(1234567890, 1), "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a Timestamp minuend", + ), + ExpressionTestCase( + "maxkey_minuend", + doc={"a": MaxKey(), "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a MaxKey minuend", + ), + ExpressionTestCase( + "minkey_minuend", + doc={"a": MinKey(), "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a MinKey minuend", + ), + ExpressionTestCase( + "uuid_minuend", + doc={"a": Binary.from_uuid(_FIXED_UUID), "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a UUID minuend", + ), + # Invalid subtrahend types + ExpressionTestCase( + "string_subtrahend", + doc={"a": 10, "b": "string"}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a string subtrahend", + ), + ExpressionTestCase( + "boolean_subtrahend", + doc={"a": 10, "b": True}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a boolean subtrahend", + ), + ExpressionTestCase( + "array_subtrahend", + doc={"a": 10, "b": [2, 3]}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject an array subtrahend", + ), + ExpressionTestCase( + "object_subtrahend", + doc={"a": 10, "b": {"x": 2}}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject an object subtrahend", + ), + ExpressionTestCase( + "empty_array_subtrahend", + doc={"a": 10, "b": []}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject an empty array subtrahend", + ), + ExpressionTestCase( + "empty_object_subtrahend", + doc={"a": 10, "b": {}}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject an empty object subtrahend", + ), + ExpressionTestCase( + "binary_subtrahend", + doc={"a": 10, "b": Binary(b"test", 0)}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a binary subtrahend", + ), + ExpressionTestCase( + "timestamp_subtrahend", + doc={"a": 10, "b": Timestamp(1234567890, 1)}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a Timestamp subtrahend", + ), + ExpressionTestCase( + "maxkey_subtrahend", + doc={"a": 10, "b": MaxKey()}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a MaxKey subtrahend", + ), + ExpressionTestCase( + "minkey_subtrahend", + doc={"a": 10, "b": MinKey()}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a MinKey subtrahend", + ), + ExpressionTestCase( + "uuid_subtrahend", + doc={"a": 10, "b": Binary.from_uuid(_FIXED_UUID)}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a UUID subtrahend", + ), + # Date arithmetic type errors + ExpressionTestCase( + "number_minus_date", + doc={"a": 1, "b": datetime(2026, 1, 1, tzinfo=timezone.utc)}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a date subtrahend when the minuend is a number", + ), + ExpressionTestCase( + "date_minus_string", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": "string"}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a string subtrahend when the minuend is a date", + ), + ExpressionTestCase( + "date_minus_bool", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": True}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject a boolean subtrahend when the minuend is a date", + ), + ExpressionTestCase( + "date_minus_array", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": [1, 2]}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_ERROR, + msg="$subtract should reject an array subtrahend when the minuend is a date", + ), + # Date with NaN/Infinity (invalid date offset) + ExpressionTestCase( + "date_minus_nan", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_NAN}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$subtract should reject NaN as a date millisecond offset", + ), + ExpressionTestCase( + "date_minus_infinity", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_INFINITY}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$subtract should reject Infinity as a date millisecond offset", + ), + ExpressionTestCase( + "date_minus_decimal_nan", + doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_NAN}, + expression={"$subtract": ["$a", "$b"]}, + error_code=TYPE_MISMATCH_DATE_ERROR, + msg="$subtract should reject Decimal128 NaN as a date millisecond offset", + ), + # Arity errors + ExpressionTestCase( + "zero_args", + doc={}, + expression={"$subtract": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$subtract should reject an empty argument list", + ), + ExpressionTestCase( + "one_arg", + doc={}, + expression={"$subtract": [1]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$subtract should reject a single argument", + ), + ExpressionTestCase( + "three_args", + doc={}, + expression={"$subtract": [1, 2, 3]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$subtract should reject three arguments", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_ERROR_TESTS)) +def test_subtract_errors(collection, test_case: ExpressionTestCase): + """Test $subtract error handling for invalid types, date constraints, and arity violations.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py new file mode 100644 index 000000000..f387c496a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py @@ -0,0 +1,90 @@ +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.error_codes import EXPRESSION_TYPE_MISMATCH_ERROR +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.aggregate + +# Property [Nested expressions]: $subtract accepts other expressions as operands. +# Property [Field path lookups]: $subtract resolves nested and deeply-nested field paths. +# Property [Array element access]: $subtract works with $arrayElemAt expressions as operands. +# Property [Composite array rejection]: $subtract rejects a composite array from $x.y on +# an array-of-objects. +SUBTRACT_INPUT_FORM_TESTS: list[ExpressionTestCase] = [ + # Nested expressions + ExpressionTestCase( + "nested_subtract_2", + doc={}, + expression={"$subtract": [{"$subtract": [10, 3]}, 2]}, + expected=5, + msg="$subtract should accept a nested $subtract expression as the minuend", + ), + ExpressionTestCase( + "nested_subtract_3", + doc={}, + expression={"$subtract": [{"$subtract": [{"$subtract": [100, 10]}, 20]}, 30]}, + expected=40, + msg="$subtract should support three levels of nested $subtract expressions", + ), + # Field path lookups + ExpressionTestCase( + "nested_field", + doc={"a": {"b": 10}, "c": 3}, + expression={"$subtract": ["$a.b", "$c"]}, + expected=7, + msg="$subtract should resolve a nested field path such as $a.b", + ), + ExpressionTestCase( + "nonexistent_field", + doc={"a": {"missing": 1}, "b": 5}, + expression={"$subtract": ["$a.nonexistent", "$b"]}, + expected=None, + msg="$subtract should return null when a field path resolves to a missing field", + ), + ExpressionTestCase( + "array_index", + doc={"arr": [10, 5, 2]}, + expression={ + "$subtract": [ + {"$arrayElemAt": ["$arr", 0]}, + {"$arrayElemAt": ["$arr", 1]}, + ] + }, + expected=5, + msg="$subtract should support $arrayElemAt expressions as operands", + ), + ExpressionTestCase( + "deeply_nested_field", + doc={"a": {"b": {"c": {"d": 20}}}}, + expression={"$subtract": ["$a.b.c.d", 8]}, + expected=12, + msg="$subtract should resolve a deeply nested field path", + ), + # Composite array rejection + ExpressionTestCase( + "composite_array_field", + doc={"x": [{"y": 10}, {"y": 3}]}, + expression={"$subtract": "$x.y"}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$subtract should reject a composite array produced by $x.y on an array-of-objects", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_INPUT_FORM_TESTS)) +def test_subtract_input_forms(collection, test_case: ExpressionTestCase): + """Test $subtract with various expression input forms and field path lookups.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py new file mode 100644 index 000000000..699d1995a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py @@ -0,0 +1,178 @@ +import math + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +pytestmark = pytest.mark.aggregate + +# Property [Infinity propagation]: $subtract propagates Infinity when one operand is infinite. +# Property [NaN propagation]: $subtract propagates NaN when either operand is NaN. +# Property [Inf - Inf = NaN]: subtracting equal signed infinities produces NaN. +SUBTRACT_NON_FINITE_TESTS: list[ExpressionTestCase] = [ + # Float Infinity + ExpressionTestCase( + "infinity_minuend", + doc={"a": FLOAT_INFINITY, "b": 1}, + expression={"$subtract": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$subtract of Infinity minus a number should return Infinity", + ), + ExpressionTestCase( + "negative_infinity_minuend", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 1}, + expression={"$subtract": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$subtract of -Infinity minus a number should return -Infinity", + ), + ExpressionTestCase( + "infinity_subtrahend", + doc={"a": 1, "b": FLOAT_INFINITY}, + expression={"$subtract": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$subtract of a number minus Infinity should return -Infinity", + ), + ExpressionTestCase( + "negative_infinity_subtrahend", + doc={"a": 1, "b": FLOAT_NEGATIVE_INFINITY}, + expression={"$subtract": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$subtract of a number minus -Infinity should return Infinity", + ), + ExpressionTestCase( + "inf_minus_zero", + doc={"a": FLOAT_INFINITY, "b": 0}, + expression={"$subtract": ["$a", "$b"]}, + expected=FLOAT_INFINITY, + msg="$subtract of Infinity minus zero should return Infinity", + ), + ExpressionTestCase( + "neg_inf_minus_zero", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 0}, + expression={"$subtract": ["$a", "$b"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$subtract of -Infinity minus zero should return -Infinity", + ), + ExpressionTestCase( + "inf_minus_inf", + doc={"a": FLOAT_INFINITY, "b": FLOAT_INFINITY}, + expression={"$subtract": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$subtract of Infinity minus Infinity should return NaN", + ), + ExpressionTestCase( + "neg_inf_minus_neg_inf", + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": FLOAT_NEGATIVE_INFINITY}, + expression={"$subtract": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$subtract of -Infinity minus -Infinity should return NaN", + ), + # Decimal128 Infinity + ExpressionTestCase( + "decimal_infinity_minuend", + doc={"a": DECIMAL128_INFINITY, "b": 1}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_INFINITY, + msg="$subtract of Decimal128 Infinity minus a number should return Decimal128 Infinity", + ), + ExpressionTestCase( + "decimal_negative_infinity_minuend", + doc={"a": DECIMAL128_NEGATIVE_INFINITY, "b": 1}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="$subtract of Decimal128 -Infinity minus a number should return Decimal128 -Infinity", + ), + ExpressionTestCase( + "decimal_inf_minus_inf", + doc={"a": DECIMAL128_INFINITY, "b": DECIMAL128_INFINITY}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$subtract of Decimal128 Infinity minus Decimal128 Infinity should return Decimal128 NaN", # noqa: E501 + ), + # Float NaN + ExpressionTestCase( + "nan_minuend", + doc={"a": FLOAT_NAN, "b": 1}, + expression={"$subtract": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$subtract with a NaN minuend should return NaN", + ), + ExpressionTestCase( + "nan_subtrahend", + doc={"a": 10, "b": FLOAT_NAN}, + expression={"$subtract": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$subtract with a NaN subtrahend should return NaN", + ), + ExpressionTestCase( + "both_nan", + doc={"a": FLOAT_NAN, "b": FLOAT_NAN}, + expression={"$subtract": ["$a", "$b"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$subtract with both operands as NaN should return NaN", + ), + # Decimal128 NaN + ExpressionTestCase( + "decimal_nan_minuend", + doc={"a": DECIMAL128_NAN, "b": 1}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$subtract with a Decimal128 NaN minuend should return Decimal128 NaN", + ), + ExpressionTestCase( + "decimal_nan_subtrahend", + doc={"a": 10, "b": DECIMAL128_NAN}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$subtract with a Decimal128 NaN subtrahend should return Decimal128 NaN", + ), + # Cross-type NaN and Infinity + ExpressionTestCase( + "decimal_nan_minus_double", + doc={"a": DECIMAL128_NAN, "b": 1.5}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$subtract of Decimal128 NaN minus a double should return Decimal128 NaN", + ), + ExpressionTestCase( + "double_minus_decimal_nan", + doc={"a": 1.5, "b": DECIMAL128_NAN}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_NAN, + msg="$subtract of a double minus Decimal128 NaN should return Decimal128 NaN", + ), + ExpressionTestCase( + "int_minus_decimal_inf", + doc={"a": 1, "b": DECIMAL128_INFINITY}, + expression={"$subtract": ["$a", "$b"]}, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="$subtract of an int minus Decimal128 Infinity should return Decimal128 -Infinity", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_NON_FINITE_TESTS)) +def test_subtract_non_finite(collection, test_case: ExpressionTestCase): + """Test $subtract behavior with non-finite values (NaN and Infinity).""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py new file mode 100644 index 000000000..9503d670a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py @@ -0,0 +1,80 @@ +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import MISSING + +pytestmark = pytest.mark.aggregate + +# Property [Null propagation]: $subtract returns null when either operand is null or missing. +# Property [Null short-circuit]: a null/missing operand short-circuits evaluation before type +# checking. +SUBTRACT_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_subtrahend", + doc={"a": 10, "b": None}, + expression={"$subtract": ["$a", "$b"]}, + expected=None, + msg="$subtract should return null when the subtrahend is null", + ), + ExpressionTestCase( + "null_minuend", + doc={"a": None, "b": 5}, + expression={"$subtract": ["$a", "$b"]}, + expected=None, + msg="$subtract should return null when the minuend is null", + ), + ExpressionTestCase( + "both_null", + doc={"a": None, "b": None}, + expression={"$subtract": ["$a", "$b"]}, + expected=None, + msg="$subtract should return null when both operands are null", + ), + ExpressionTestCase( + "missing_subtrahend", + doc={"a": 10}, + expression={"$subtract": ["$a", MISSING]}, + expected=None, + msg="$subtract should return null when the subtrahend field is missing", + ), + ExpressionTestCase( + "missing_minuend", + doc={"b": 5}, + expression={"$subtract": [MISSING, "$b"]}, + expected=None, + msg="$subtract should return null when the minuend field is missing", + ), + ExpressionTestCase( + "null_with_non_numeric", + doc={"a": None}, + expression={"$subtract": ["$a", "string"]}, + expected=None, + msg="$subtract should return null when a null minuend short-circuits type checking", + ), + ExpressionTestCase( + "missing_with_non_numeric", + doc={}, + expression={"$subtract": [MISSING, True]}, + expected=None, + msg="$subtract should return null when a missing minuend short-circuits type checking", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_NULL_TESTS)) +def test_subtract_null(collection, test_case: ExpressionTestCase): + """Test $subtract null and missing field propagation.""" + result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) + assert_expression_result( + result, + expected=test_case.expected, + error_code=test_case.error_code, + msg=test_case.msg, + ) From 7b6505ce7a118e39f6e11bce45352392c5343d70 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 10 Jul 2026 09:39:21 -0700 Subject: [PATCH 06/10] Added init Signed-off-by: PatersonProjects --- .../core/operator/expressions/arithmetic/subtract/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/__init__.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/__init__.py new file mode 100644 index 000000000..e69de29bb From 7acfe8653424682cc4fa5d90976ac4411e577b2b Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 10 Jul 2026 10:04:39 -0700 Subject: [PATCH 07/10] Fixed failing tests Signed-off-by: PatersonProjects --- .../subtract/test_subtract_boundaries.py | 14 +++++++------- .../arithmetic/subtract/test_subtract_dates.py | 4 ++-- .../arithmetic/subtract/test_subtract_errors.py | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py index 180db3508..8c591741c 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py @@ -60,14 +60,14 @@ "int32_overflow", doc={"a": INT32_MAX, "b": -1}, expression={"$subtract": ["$a", "$b"]}, - expected=INT32_OVERFLOW, + expected=Int64(INT32_OVERFLOW), msg="$subtract should promote to int64 when the int32 result exceeds INT32_MAX", ), ExpressionTestCase( "int32_underflow", doc={"a": INT32_MIN, "b": 1}, expression={"$subtract": ["$a", "$b"]}, - expected=INT32_UNDERFLOW, + expected=Int64(INT32_UNDERFLOW), msg="$subtract should promote to int64 when the int32 result is below INT32_MIN", ), # Int32 boundary values @@ -89,14 +89,14 @@ "int32_min_minuend", doc={"a": INT32_MIN, "b": 10}, expression={"$subtract": ["$a", "$b"]}, - expected=-2147483658, + expected=Int64(-2147483658), msg="$subtract should promote to int64 when subtracting from INT32_MIN", ), ExpressionTestCase( "int32_min_plus_1_minuend", doc={"a": INT32_MIN_PLUS_1, "b": 10}, expression={"$subtract": ["$a", "$b"]}, - expected=-2147483657, + expected=Int64(-2147483657), msg="$subtract should promote to int64 when subtracting from INT32_MIN + 1", ), ExpressionTestCase( @@ -110,7 +110,7 @@ "int32_min_subtrahend", doc={"a": 10, "b": INT32_MIN}, expression={"$subtract": ["$a", "$b"]}, - expected=2147483658, + expected=Int64(2147483658), msg="$subtract should promote to int64 when INT32_MIN is the subtrahend", ), # Int64 overflow and underflow @@ -210,14 +210,14 @@ ), ExpressionTestCase( "double_max_safe_integer_minuend", - doc={"a": DOUBLE_MAX_SAFE_INTEGER, "b": 1}, + doc={"a": float(DOUBLE_MAX_SAFE_INTEGER), "b": 1}, expression={"$subtract": ["$a", "$b"]}, expected=9007199254740991.0, msg="$subtract should handle the maximum safe integer double as the minuend", ), ExpressionTestCase( "double_max_safe_integer_subtrahend", - doc={"a": 1, "b": DOUBLE_MAX_SAFE_INTEGER}, + doc={"a": 1, "b": float(DOUBLE_MAX_SAFE_INTEGER)}, expression={"$subtract": ["$a", "$b"]}, expected=-9007199254740991.0, msg="$subtract should handle the maximum safe integer double as the subtrahend", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py index 657d49005..0e72412c0 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py @@ -110,7 +110,7 @@ "b": datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), }, expression={"$subtract": ["$a", "$b"]}, - expected=86400000, + expected=Int64(86400000), msg="$subtract of two dates should return the difference in milliseconds", ), ExpressionTestCase( @@ -120,7 +120,7 @@ "b": datetime(2026, 1, 2, 0, 0, 0, tzinfo=timezone.utc), }, expression={"$subtract": ["$a", "$b"]}, - expected=-86400000, + expected=Int64(-86400000), msg="$subtract of two dates should return negative ms when the minuend date is earlier", ), ] diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py index 9d2414632..ef8b66985 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py @@ -13,7 +13,7 @@ ) from documentdb_tests.framework.error_codes import ( EXPRESSION_TYPE_MISMATCH_ERROR, - TYPE_MISMATCH_DATE_ERROR, + OVERFLOW_ERROR, TYPE_MISMATCH_ERROR, ) from documentdb_tests.framework.parametrize import pytest_params @@ -223,21 +223,21 @@ "date_minus_nan", doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_NAN}, expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_DATE_ERROR, + error_code=OVERFLOW_ERROR, msg="$subtract should reject NaN as a date millisecond offset", ), ExpressionTestCase( "date_minus_infinity", doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_INFINITY}, expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_DATE_ERROR, + error_code=OVERFLOW_ERROR, msg="$subtract should reject Infinity as a date millisecond offset", ), ExpressionTestCase( "date_minus_decimal_nan", doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": DECIMAL128_NAN}, expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_DATE_ERROR, + error_code=OVERFLOW_ERROR, msg="$subtract should reject Decimal128 NaN as a date millisecond offset", ), # Arity errors From 8d105e304bcfd5b7c25055d4e34f994d602e9b06 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 10 Jul 2026 13:56:39 -0700 Subject: [PATCH 08/10] Updated to use more constants Signed-off-by: PatersonProjects --- .../subtract/test_subtract_basic.py | 61 +++++++----- .../subtract/test_subtract_boundaries.py | 80 +++++++++------- .../subtract/test_subtract_dates.py | 92 +++++++++++-------- .../subtract/test_subtract_errors.py | 32 ++++--- .../subtract/test_subtract_input_forms.py | 46 ++++++---- .../subtract/test_subtract_non_finite.py | 29 ++++-- .../arithmetic/subtract/test_subtract_null.py | 11 ++- 7 files changed, 217 insertions(+), 134 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py index 350949932..6684a6ff4 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py @@ -9,17 +9,19 @@ execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_TWO_AND_HALF, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_TWO_AND_HALF, + DOUBLE_ZERO, + INT32_ZERO, +) pytestmark = pytest.mark.aggregate # Property [Same-type arithmetic]: $subtract preserves the BSON type when both operands share # a type. -# Property [Mixed-type promotion]: $subtract promotes the result to the wider of the two input -# types. -# Property [Sign handling]: $subtract correctly computes the sign of the result. -# Property [Zero handling]: $subtract handles zero operands correctly. -SUBTRACT_BASIC_TESTS: list[ExpressionTestCase] = [ - # Same type operations +SAME_TYPE_ARITHMETIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "same_type_int32", doc={"a": 10, "b": 3}, @@ -36,7 +38,7 @@ ), ExpressionTestCase( "same_type_double", - doc={"a": 10.5, "b": 2.5}, + doc={"a": 10.5, "b": DOUBLE_TWO_AND_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=8.0, msg="$subtract should return double for double - double", @@ -48,7 +50,11 @@ expected=Decimal128("10.0"), msg="$subtract should return Decimal128 for Decimal128 - Decimal128", ), - # Mixed numeric type promotion +] + +# Property [Mixed-type promotion]: $subtract promotes the result to the wider of the two input +# types. +MIXED_TYPE_PROMOTION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "int32_int64", doc={"a": 10, "b": Int64(3)}, @@ -58,14 +64,14 @@ ), ExpressionTestCase( "int32_double", - doc={"a": 10, "b": 2.5}, + doc={"a": 10, "b": DOUBLE_TWO_AND_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=7.5, msg="$subtract should promote to double for int32 - double", ), ExpressionTestCase( "int32_decimal", - doc={"a": 10, "b": Decimal128("2.5")}, + doc={"a": 10, "b": DECIMAL128_TWO_AND_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=Decimal128("7.5"), msg="$subtract should promote to Decimal128 for int32 - Decimal128", @@ -86,12 +92,15 @@ ), ExpressionTestCase( "double_decimal", - doc={"a": 10.5, "b": Decimal128("2.5")}, + doc={"a": 10.5, "b": DECIMAL128_TWO_AND_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=Decimal128("8.0000000000000"), msg="$subtract should promote to Decimal128 for double - Decimal128", ), - # Sign handling +] + +# Property [Sign handling]: $subtract correctly computes the sign of the result. +SIGN_HANDLING_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "negative_positive", doc={"a": -10, "b": 3}, @@ -127,44 +136,54 @@ expected=-4.5, msg="$subtract should return negative double when minuend is smaller than subtrahend", ), - # Zero handling +] + +# Property [Zero handling]: $subtract handles zero operands correctly. +ZERO_HANDLING_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "zero_minuend", - doc={"a": 0, "b": 5}, + doc={"a": INT32_ZERO, "b": 5}, expression={"$subtract": ["$a", "$b"]}, expected=-5, msg="$subtract should return negated subtrahend when the minuend is zero", ), ExpressionTestCase( "zero_subtrahend", - doc={"a": 5, "b": 0}, + doc={"a": 5, "b": INT32_ZERO}, expression={"$subtract": ["$a", "$b"]}, expected=5, msg="$subtract should return the minuend unchanged when the subtrahend is zero", ), ExpressionTestCase( "zeros", - doc={"a": 0, "b": 0}, + doc={"a": INT32_ZERO, "b": INT32_ZERO}, expression={"$subtract": ["$a", "$b"]}, - expected=0, + expected=INT32_ZERO, msg="$subtract should return zero for zero - zero", ), ExpressionTestCase( "zero_negative_zero", - doc={"a": 0, "b": -0.0}, + doc={"a": INT32_ZERO, "b": DOUBLE_NEGATIVE_ZERO}, expression={"$subtract": ["$a", "$b"]}, - expected=0.0, + expected=DOUBLE_ZERO, msg="$subtract of int32 zero minus double negative-zero should return double positive-zero", ), ExpressionTestCase( "negative_zero_zero", - doc={"a": -0.0, "b": 0}, + doc={"a": DOUBLE_NEGATIVE_ZERO, "b": INT32_ZERO}, expression={"$subtract": ["$a", "$b"]}, - expected=-0.0, + expected=DOUBLE_NEGATIVE_ZERO, msg="$subtract of double negative-zero minus int32 zero should return double negative-zero", ), ] +SUBTRACT_BASIC_TESTS: list[ExpressionTestCase] = ( + SAME_TYPE_ARITHMETIC_TESTS + + MIXED_TYPE_PROMOTION_TESTS + + SIGN_HANDLING_TESTS + + ZERO_HANDLING_TESTS +) + @pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_BASIC_TESTS)) def test_subtract_basic(collection, test_case: ExpressionTestCase): diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py index 8c591741c..8dcf30705 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py @@ -23,6 +23,8 @@ DECIMAL128_SMALL_EXPONENT, DECIMAL128_TRAILING_ZERO, DECIMAL128_TWO_AND_HALF, + DECIMAL128_ZERO, + DOUBLE_FROM_INT64_MAX, DOUBLE_HALF, DOUBLE_JUST_ABOVE_HALF, DOUBLE_JUST_BELOW_HALF, @@ -42,6 +44,7 @@ INT32_MIN_PLUS_1, INT32_OVERFLOW, INT32_UNDERFLOW, + INT32_ZERO, INT64_MAX, INT64_MAX_MINUS_1, INT64_MIN, @@ -51,11 +54,7 @@ pytestmark = pytest.mark.aggregate # Property [Int32 overflow]: $subtract promotes int32 results to int64 on overflow/underflow. -# Property [Int64 overflow]: $subtract promotes int64 results to double on overflow/underflow. -# Property [Double overflow]: $subtract returns ±Infinity on double overflow. -# Property [Decimal128 precision]: $subtract preserves Decimal128 full precision. -SUBTRACT_BOUNDARY_TESTS: list[ExpressionTestCase] = [ - # Int32 overflow and underflow +INT32_OVERFLOW_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "int32_overflow", doc={"a": INT32_MAX, "b": -1}, @@ -113,19 +112,22 @@ expected=Int64(2147483658), msg="$subtract should promote to int64 when INT32_MIN is the subtrahend", ), - # Int64 overflow and underflow +] + +# Property [Int64 overflow]: $subtract promotes int64 results to double on overflow/underflow. +INT64_OVERFLOW_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "int64_overflow", doc={"a": INT64_MAX, "b": -1}, expression={"$subtract": ["$a", "$b"]}, - expected=9.223372036854776e18, + expected=DOUBLE_FROM_INT64_MAX, msg="$subtract should promote to double when the int64 result exceeds INT64_MAX", ), ExpressionTestCase( "int64_underflow", doc={"a": INT64_MIN, "b": 1}, expression={"$subtract": ["$a", "$b"]}, - expected=-9.223372036854776e18, + expected=-DOUBLE_FROM_INT64_MAX, msg="$subtract should promote to double when the int64 result is below INT64_MIN", ), # Int64 boundary values @@ -147,14 +149,14 @@ "int64_min_minuend", doc={"a": INT64_MIN, "b": Int64(10)}, expression={"$subtract": ["$a", "$b"]}, - expected=-9.223372036854776e18, + expected=-DOUBLE_FROM_INT64_MAX, msg="$subtract should overflow to double when subtracting from INT64_MIN", ), ExpressionTestCase( "int64_min_plus_1_minuend", doc={"a": INT64_MIN_PLUS_1, "b": Int64(10)}, expression={"$subtract": ["$a", "$b"]}, - expected=-9.223372036854776e18, + expected=-DOUBLE_FROM_INT64_MAX, msg="$subtract should overflow to double when subtracting from INT64_MIN + 1", ), ExpressionTestCase( @@ -168,20 +170,23 @@ "int64_min_subtrahend", doc={"a": Int64(10), "b": INT64_MIN}, expression={"$subtract": ["$a", "$b"]}, - expected=9.223372036854776e18, + expected=DOUBLE_FROM_INT64_MAX, msg="$subtract should overflow to double when INT64_MIN is the subtrahend", ), - # Double overflow +] + +# Property [Double overflow]: $subtract returns ±Infinity on double overflow. +DOUBLE_OVERFLOW_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "double_overflow", - doc={"a": 1e308, "b": -1e308}, + doc={"a": DOUBLE_NEAR_MAX, "b": -DOUBLE_NEAR_MAX}, expression={"$subtract": ["$a", "$b"]}, expected=FLOAT_INFINITY, msg="$subtract should return Infinity on double overflow", ), ExpressionTestCase( "double_underflow", - doc={"a": -1e308, "b": 1e308}, + doc={"a": -DOUBLE_NEAR_MAX, "b": DOUBLE_NEAR_MAX}, expression={"$subtract": ["$a", "$b"]}, expected=FLOAT_NEGATIVE_INFINITY, msg="$subtract should return -Infinity on double underflow", @@ -189,16 +194,16 @@ # Double boundary values ExpressionTestCase( "double_min_subnormal_minuend", - doc={"a": DOUBLE_MIN_SUBNORMAL, "b": 0}, + doc={"a": DOUBLE_MIN_SUBNORMAL, "b": INT32_ZERO}, expression={"$subtract": ["$a", "$b"]}, - expected=5e-324, + expected=DOUBLE_MIN_SUBNORMAL, msg="$subtract should handle the minimum subnormal double value", ), ExpressionTestCase( "double_near_min_minuend", - doc={"a": DOUBLE_NEAR_MIN, "b": 0}, + doc={"a": DOUBLE_NEAR_MIN, "b": INT32_ZERO}, expression={"$subtract": ["$a", "$b"]}, - expected=1e-308, + expected=DOUBLE_NEAR_MIN, msg="$subtract should handle doubles near the minimum normal value", ), ExpressionTestCase( @@ -222,7 +227,10 @@ expected=-9007199254740991.0, msg="$subtract should handle the maximum safe integer double as the subtrahend", ), - # Decimal128 boundary values +] + +# Property [Decimal128 precision]: $subtract preserves Decimal128 full precision. +DECIMAL128_PRECISION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "decimal128_max_minuend", doc={"a": DECIMAL128_MAX, "b": Decimal128("1")}, @@ -239,7 +247,7 @@ ), ExpressionTestCase( "decimal128_small_exponent_minuend", - doc={"a": DECIMAL128_SMALL_EXPONENT, "b": Decimal128("0")}, + doc={"a": DECIMAL128_SMALL_EXPONENT, "b": DECIMAL128_ZERO}, expression={"$subtract": ["$a", "$b"]}, expected=DECIMAL128_SMALL_EXPONENT, msg="$subtract should handle Decimal128 with a small exponent", @@ -253,14 +261,14 @@ ), ExpressionTestCase( "decimal128_trailing_zero", - doc={"a": DECIMAL128_TRAILING_ZERO, "b": Decimal128("0.5")}, + doc={"a": DECIMAL128_TRAILING_ZERO, "b": DECIMAL128_HALF}, expression={"$subtract": ["$a", "$b"]}, - expected=Decimal128("0.5"), + expected=DECIMAL128_HALF, msg="$subtract should handle Decimal128 with a trailing zero", ), ExpressionTestCase( "decimal128_many_trailing_zeros", - doc={"a": DECIMAL128_MANY_TRAILING_ZEROS, "b": Decimal128("0.5")}, + doc={"a": DECIMAL128_MANY_TRAILING_ZEROS, "b": DECIMAL128_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=Decimal128("0.50000000000000000000000000000000"), msg="$subtract should handle Decimal128 with many trailing zeros", @@ -268,7 +276,7 @@ # Decimal128 precision ExpressionTestCase( "decimal_precision", - doc={"a": Decimal128("10.5"), "b": Decimal128("2.5")}, + doc={"a": Decimal128("10.5"), "b": DECIMAL128_TWO_AND_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=Decimal128("8.0"), msg="$subtract should preserve Decimal128 precision", @@ -300,28 +308,28 @@ ), ExpressionTestCase( "double_one_and_half_minuend", - doc={"a": DOUBLE_ONE_AND_HALF, "b": 0.5}, + doc={"a": DOUBLE_ONE_AND_HALF, "b": DOUBLE_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=1.0, msg="$subtract should correctly compute 1.5 - 0.5 = 1.0", ), ExpressionTestCase( "double_two_and_half_minuend", - doc={"a": DOUBLE_TWO_AND_HALF, "b": 1.5}, + doc={"a": DOUBLE_TWO_AND_HALF, "b": DOUBLE_ONE_AND_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=1.0, msg="$subtract should correctly compute 2.5 - 1.5 = 1.0", ), ExpressionTestCase( "double_negative_half_minuend", - doc={"a": DOUBLE_NEGATIVE_HALF, "b": 0.5}, + doc={"a": DOUBLE_NEGATIVE_HALF, "b": DOUBLE_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=-1.0, msg="$subtract should correctly compute -0.5 - 0.5 = -1.0", ), ExpressionTestCase( "double_negative_one_and_half_minuend", - doc={"a": DOUBLE_NEGATIVE_ONE_AND_HALF, "b": 0.5}, + doc={"a": DOUBLE_NEGATIVE_ONE_AND_HALF, "b": DOUBLE_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=-2.0, msg="$subtract should correctly compute -1.5 - 0.5 = -2.0", @@ -350,28 +358,28 @@ ), ExpressionTestCase( "decimal_one_and_half_minuend", - doc={"a": DECIMAL128_ONE_AND_HALF, "b": Decimal128("0.5")}, + doc={"a": DECIMAL128_ONE_AND_HALF, "b": DECIMAL128_HALF}, expression={"$subtract": ["$a", "$b"]}, - expected=Decimal128("1.0"), + expected=DECIMAL128_TRAILING_ZERO, msg="$subtract should correctly compute Decimal128 1.5 - 0.5 = 1.0", ), ExpressionTestCase( "decimal_two_and_half_minuend", - doc={"a": DECIMAL128_TWO_AND_HALF, "b": Decimal128("1.5")}, + doc={"a": DECIMAL128_TWO_AND_HALF, "b": DECIMAL128_ONE_AND_HALF}, expression={"$subtract": ["$a", "$b"]}, - expected=Decimal128("1.0"), + expected=DECIMAL128_TRAILING_ZERO, msg="$subtract should correctly compute Decimal128 2.5 - 1.5 = 1.0", ), ExpressionTestCase( "decimal_negative_half_minuend", - doc={"a": DECIMAL128_NEGATIVE_HALF, "b": Decimal128("0.5")}, + doc={"a": DECIMAL128_NEGATIVE_HALF, "b": DECIMAL128_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=Decimal128("-1.0"), msg="$subtract should correctly compute Decimal128 -0.5 - 0.5 = -1.0", ), ExpressionTestCase( "decimal_negative_one_and_half_minuend", - doc={"a": DECIMAL128_NEGATIVE_ONE_AND_HALF, "b": Decimal128("0.5")}, + doc={"a": DECIMAL128_NEGATIVE_ONE_AND_HALF, "b": DECIMAL128_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=Decimal128("-2.0"), msg="$subtract should correctly compute Decimal128 -1.5 - 0.5 = -2.0", @@ -392,6 +400,10 @@ ), ] +SUBTRACT_BOUNDARY_TESTS: list[ExpressionTestCase] = ( + INT32_OVERFLOW_TESTS + INT64_OVERFLOW_TESTS + DOUBLE_OVERFLOW_TESTS + DECIMAL128_PRECISION_TESTS +) + @pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_BOUNDARY_TESTS)) def test_subtract_boundaries(collection, test_case: ExpressionTestCase): diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py index 0e72412c0..9c3f4373e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py @@ -1,7 +1,7 @@ from datetime import datetime, timezone import pytest -from bson import Decimal128, Int64 +from bson import Int64 from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 ExpressionTestCase, @@ -11,16 +11,18 @@ execute_expression_with_insert, ) from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_ONE_AND_HALF, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_TWO_AND_HALF, + INT32_ZERO, +) pytestmark = pytest.mark.aggregate # Property [Date - numeric]: $subtract returns a date when the minuend is a date and subtrahend # is numeric (milliseconds). -# Property [Date - date]: $subtract returns the difference in milliseconds when both operands -# are dates. -# Property [Date rounding]: fractional ms in the subtrahend are rounded using round-half-to-even. -SUBTRACT_DATE_TESTS: list[ExpressionTestCase] = [ - # Date minus numeric types +DATE_NUMERIC_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "date_int32", doc={ @@ -41,36 +43,6 @@ expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), msg="$subtract should subtract int64 milliseconds from a date", ), - ExpressionTestCase( - "date_decimal", - doc={ - "a": datetime(2026, 1, 1, 0, 0, 1, tzinfo=timezone.utc), - "b": Decimal128("1.5"), - }, - expression={"$subtract": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 998000, tzinfo=timezone.utc), - msg="$subtract should round Decimal128 1.5 ms to 2 ms before subtracting from a date", - ), - ExpressionTestCase( - "date_double_round_up", - doc={ - "a": datetime(2026, 1, 1, 0, 0, 0, 3000, tzinfo=timezone.utc), - "b": 2.5, - }, - expression={"$subtract": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), - msg="$subtract should round double 2.5 ms to 3 ms (round-half-to-even) before subtracting", - ), - ExpressionTestCase( - "date_double_truncates", - doc={ - "a": datetime(2026, 1, 1, 0, 0, 0, 5000, tzinfo=timezone.utc), - "b": 4.4, - }, - expression={"$subtract": ["$a", "$b"]}, - expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), - msg="$subtract should round double 4.4 ms to 4 ms before subtracting from a date", - ), # Date minus negative number (moves date forward) ExpressionTestCase( "date_negative", @@ -86,7 +58,7 @@ "date_zero", doc={ "a": datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), - "b": 0, + "b": INT32_ZERO, }, expression={"$subtract": ["$a", "$b"]}, expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), @@ -96,13 +68,17 @@ "date_negative_zero", doc={ "a": datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), - "b": -0.0, + "b": DOUBLE_NEGATIVE_ZERO, }, expression={"$subtract": ["$a", "$b"]}, expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), msg="$subtract should return the same date when subtracting negative-zero milliseconds", ), - # Date minus date returns milliseconds +] + +# Property [Date - date]: $subtract returns the difference in milliseconds when both operands +# are dates. +DATE_DATE_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "two_dates", doc={ @@ -125,6 +101,44 @@ ), ] +# Property [Date rounding]: fractional ms in the subtrahend are rounded using round-half-to-even. +DATE_ROUNDING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "date_decimal", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 1, tzinfo=timezone.utc), + "b": DECIMAL128_ONE_AND_HALF, + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 998000, tzinfo=timezone.utc), + msg="$subtract should round Decimal128 1.5 ms to 2 ms before subtracting from a date", + ), + ExpressionTestCase( + "date_double_round_up", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 0, 3000, tzinfo=timezone.utc), + "b": DOUBLE_TWO_AND_HALF, + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="$subtract should round double 2.5 ms to 3 ms (round-half-to-even) before subtracting", + ), + ExpressionTestCase( + "date_double_truncates", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 0, 5000, tzinfo=timezone.utc), + "b": 4.4, + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), + msg="$subtract should round double 4.4 ms to 4 ms before subtracting from a date", + ), +] + +SUBTRACT_DATE_TESTS: list[ExpressionTestCase] = ( + DATE_NUMERIC_TESTS + DATE_DATE_TESTS + DATE_ROUNDING_TESTS +) + @pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_DATE_TESTS)) def test_subtract_dates(collection, test_case: ExpressionTestCase): diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py index ef8b66985..7e5042751 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py @@ -21,6 +21,7 @@ DECIMAL128_NAN, FLOAT_INFINITY, FLOAT_NAN, + INT32_ZERO, ) pytestmark = pytest.mark.aggregate @@ -28,12 +29,7 @@ _FIXED_UUID = uuid.UUID("550e8400-e29b-41d4-a716-446655440000") # Property [Type rejection]: $subtract rejects non-numeric, non-date types with TYPE_MISMATCH_ERROR. -# Property [Date constraint]: $subtract enforces date arithmetic type rules. -# Property [Date NaN/Inf]: $subtract rejects NaN/Infinity as a date offset with -# TYPE_MISMATCH_DATE_ERROR. -# Property [Arity]: $subtract requires exactly two arguments. -SUBTRACT_ERROR_TESTS: list[ExpressionTestCase] = [ - # Invalid minuend types +TYPE_REJECTION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "string_minuend", doc={"a": "string", "b": 5}, @@ -78,7 +74,7 @@ ), ExpressionTestCase( "binary_minuend", - doc={"a": Binary(b"test", 0), "b": 5}, + doc={"a": Binary(b"test", INT32_ZERO), "b": 5}, expression={"$subtract": ["$a", "$b"]}, error_code=TYPE_MISMATCH_ERROR, msg="$subtract should reject a binary minuend", @@ -156,7 +152,7 @@ ), ExpressionTestCase( "binary_subtrahend", - doc={"a": 10, "b": Binary(b"test", 0)}, + doc={"a": 10, "b": Binary(b"test", INT32_ZERO)}, expression={"$subtract": ["$a", "$b"]}, error_code=TYPE_MISMATCH_ERROR, msg="$subtract should reject a binary subtrahend", @@ -189,7 +185,10 @@ error_code=TYPE_MISMATCH_ERROR, msg="$subtract should reject a UUID subtrahend", ), - # Date arithmetic type errors +] + +# Property [Date constraint]: $subtract enforces date arithmetic type rules. +DATE_CONSTRAINT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "number_minus_date", doc={"a": 1, "b": datetime(2026, 1, 1, tzinfo=timezone.utc)}, @@ -218,7 +217,11 @@ error_code=TYPE_MISMATCH_ERROR, msg="$subtract should reject an array subtrahend when the minuend is a date", ), - # Date with NaN/Infinity (invalid date offset) +] + +# Property [Date NaN/Inf]: $subtract rejects NaN/Infinity as a date offset with +# TYPE_MISMATCH_DATE_ERROR. +DATE_NAN_INF_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "date_minus_nan", doc={"a": datetime(2026, 1, 1, tzinfo=timezone.utc), "b": FLOAT_NAN}, @@ -240,7 +243,10 @@ error_code=OVERFLOW_ERROR, msg="$subtract should reject Decimal128 NaN as a date millisecond offset", ), - # Arity errors +] + +# Property [Arity]: $subtract requires exactly two arguments. +ARITY_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "zero_args", doc={}, @@ -264,6 +270,10 @@ ), ] +SUBTRACT_ERROR_TESTS: list[ExpressionTestCase] = ( + TYPE_REJECTION_TESTS + DATE_CONSTRAINT_TESTS + DATE_NAN_INF_TESTS + ARITY_TESTS +) + @pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_ERROR_TESTS)) def test_subtract_errors(collection, test_case: ExpressionTestCase): diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py index f387c496a..a29f8e5c6 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py @@ -9,16 +9,12 @@ ) from documentdb_tests.framework.error_codes import EXPRESSION_TYPE_MISMATCH_ERROR from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import INT32_ZERO pytestmark = pytest.mark.aggregate # Property [Nested expressions]: $subtract accepts other expressions as operands. -# Property [Field path lookups]: $subtract resolves nested and deeply-nested field paths. -# Property [Array element access]: $subtract works with $arrayElemAt expressions as operands. -# Property [Composite array rejection]: $subtract rejects a composite array from $x.y on -# an array-of-objects. -SUBTRACT_INPUT_FORM_TESTS: list[ExpressionTestCase] = [ - # Nested expressions +NESTED_EXPRESSION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "nested_subtract_2", doc={}, @@ -33,7 +29,10 @@ expected=40, msg="$subtract should support three levels of nested $subtract expressions", ), - # Field path lookups +] + +# Property [Field path lookups]: $subtract resolves nested and deeply-nested field paths. +FIELD_PATH_LOOKUP_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "nested_field", doc={"a": {"b": 10}, "c": 3}, @@ -48,26 +47,34 @@ expected=None, msg="$subtract should return null when a field path resolves to a missing field", ), + ExpressionTestCase( + "deeply_nested_field", + doc={"a": {"b": {"c": {"d": 20}}}}, + expression={"$subtract": ["$a.b.c.d", 8]}, + expected=12, + msg="$subtract should resolve a deeply nested field path", + ), +] + +# Property [Array element access]: $subtract works with $arrayElemAt expressions as operands. +ARRAY_ELEMENT_ACCESS_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "array_index", doc={"arr": [10, 5, 2]}, expression={ "$subtract": [ - {"$arrayElemAt": ["$arr", 0]}, + {"$arrayElemAt": ["$arr", INT32_ZERO]}, {"$arrayElemAt": ["$arr", 1]}, ] }, expected=5, msg="$subtract should support $arrayElemAt expressions as operands", ), - ExpressionTestCase( - "deeply_nested_field", - doc={"a": {"b": {"c": {"d": 20}}}}, - expression={"$subtract": ["$a.b.c.d", 8]}, - expected=12, - msg="$subtract should resolve a deeply nested field path", - ), - # Composite array rejection +] + +# Property [Composite array rejection]: $subtract rejects a composite array from $x.y on +# an array-of-objects. +COMPOSITE_ARRAY_REJECTION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "composite_array_field", doc={"x": [{"y": 10}, {"y": 3}]}, @@ -77,6 +84,13 @@ ), ] +SUBTRACT_INPUT_FORM_TESTS: list[ExpressionTestCase] = ( + NESTED_EXPRESSION_TESTS + + FIELD_PATH_LOOKUP_TESTS + + ARRAY_ELEMENT_ACCESS_TESTS + + COMPOSITE_ARRAY_REJECTION_TESTS +) + @pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_INPUT_FORM_TESTS)) def test_subtract_input_forms(collection, test_case: ExpressionTestCase): diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py index 699d1995a..20bd2024b 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py @@ -14,18 +14,17 @@ DECIMAL128_INFINITY, DECIMAL128_NAN, DECIMAL128_NEGATIVE_INFINITY, + DOUBLE_ONE_AND_HALF, FLOAT_INFINITY, FLOAT_NAN, FLOAT_NEGATIVE_INFINITY, + INT32_ZERO, ) pytestmark = pytest.mark.aggregate # Property [Infinity propagation]: $subtract propagates Infinity when one operand is infinite. -# Property [NaN propagation]: $subtract propagates NaN when either operand is NaN. -# Property [Inf - Inf = NaN]: subtracting equal signed infinities produces NaN. -SUBTRACT_NON_FINITE_TESTS: list[ExpressionTestCase] = [ - # Float Infinity +INFINITY_PROPAGATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "infinity_minuend", doc={"a": FLOAT_INFINITY, "b": 1}, @@ -56,14 +55,14 @@ ), ExpressionTestCase( "inf_minus_zero", - doc={"a": FLOAT_INFINITY, "b": 0}, + doc={"a": FLOAT_INFINITY, "b": INT32_ZERO}, expression={"$subtract": ["$a", "$b"]}, expected=FLOAT_INFINITY, msg="$subtract of Infinity minus zero should return Infinity", ), ExpressionTestCase( "neg_inf_minus_zero", - doc={"a": FLOAT_NEGATIVE_INFINITY, "b": 0}, + doc={"a": FLOAT_NEGATIVE_INFINITY, "b": INT32_ZERO}, expression={"$subtract": ["$a", "$b"]}, expected=FLOAT_NEGATIVE_INFINITY, msg="$subtract of -Infinity minus zero should return -Infinity", @@ -104,7 +103,10 @@ expected=DECIMAL128_NAN, msg="$subtract of Decimal128 Infinity minus Decimal128 Infinity should return Decimal128 NaN", # noqa: E501 ), - # Float NaN +] + +# Property [NaN propagation]: $subtract propagates NaN when either operand is NaN. +NAN_PROPAGATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "nan_minuend", doc={"a": FLOAT_NAN, "b": 1}, @@ -141,17 +143,20 @@ expected=DECIMAL128_NAN, msg="$subtract with a Decimal128 NaN subtrahend should return Decimal128 NaN", ), - # Cross-type NaN and Infinity +] + +# Property [Inf - Inf = NaN]: subtracting equal signed infinities produces NaN. +INF_MINUS_INF_NAN_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "decimal_nan_minus_double", - doc={"a": DECIMAL128_NAN, "b": 1.5}, + doc={"a": DECIMAL128_NAN, "b": DOUBLE_ONE_AND_HALF}, expression={"$subtract": ["$a", "$b"]}, expected=DECIMAL128_NAN, msg="$subtract of Decimal128 NaN minus a double should return Decimal128 NaN", ), ExpressionTestCase( "double_minus_decimal_nan", - doc={"a": 1.5, "b": DECIMAL128_NAN}, + doc={"a": DOUBLE_ONE_AND_HALF, "b": DECIMAL128_NAN}, expression={"$subtract": ["$a", "$b"]}, expected=DECIMAL128_NAN, msg="$subtract of a double minus Decimal128 NaN should return Decimal128 NaN", @@ -165,6 +170,10 @@ ), ] +SUBTRACT_NON_FINITE_TESTS: list[ExpressionTestCase] = ( + INFINITY_PROPAGATION_TESTS + NAN_PROPAGATION_TESTS + INF_MINUS_INF_NAN_TESTS +) + @pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_NON_FINITE_TESTS)) def test_subtract_non_finite(collection, test_case: ExpressionTestCase): diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py index 9503d670a..28696146f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py @@ -13,9 +13,7 @@ pytestmark = pytest.mark.aggregate # Property [Null propagation]: $subtract returns null when either operand is null or missing. -# Property [Null short-circuit]: a null/missing operand short-circuits evaluation before type -# checking. -SUBTRACT_NULL_TESTS: list[ExpressionTestCase] = [ +NULL_PROPAGATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "null_subtrahend", doc={"a": 10, "b": None}, @@ -51,6 +49,11 @@ expected=None, msg="$subtract should return null when the minuend field is missing", ), +] + +# Property [Null short-circuit]: a null/missing operand short-circuits evaluation before type +# checking. +NULL_SHORT_CIRCUIT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "null_with_non_numeric", doc={"a": None}, @@ -67,6 +70,8 @@ ), ] +SUBTRACT_NULL_TESTS: list[ExpressionTestCase] = NULL_PROPAGATION_TESTS + NULL_SHORT_CIRCUIT_TESTS + @pytest.mark.parametrize("test_case", pytest_params(SUBTRACT_NULL_TESTS)) def test_subtract_null(collection, test_case: ExpressionTestCase): From 550fd9e4de7ea63fac7da0ae2dbbb86eb8a5c39e Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 10 Jul 2026 14:21:58 -0700 Subject: [PATCH 09/10] Addressed PR Comments Signed-off-by: PatersonProjects --- .../subtract/test_subtract_basic.py | 2 - .../subtract/test_subtract_boundaries.py | 2 - .../subtract/test_subtract_bson_types.py | 15 +- .../subtract/test_subtract_dates.py | 8 +- .../subtract/test_subtract_errors.py | 168 +----------------- .../subtract/test_subtract_input_forms.py | 2 - .../subtract/test_subtract_non_finite.py | 2 - .../arithmetic/subtract/test_subtract_null.py | 2 - 8 files changed, 13 insertions(+), 188 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py index 6684a6ff4..2556eef3e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_basic.py @@ -17,8 +17,6 @@ INT32_ZERO, ) -pytestmark = pytest.mark.aggregate - # Property [Same-type arithmetic]: $subtract preserves the BSON type when both operands share # a type. SAME_TYPE_ARITHMETIC_TESTS: list[ExpressionTestCase] = [ diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py index 8dcf30705..405e382e6 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_boundaries.py @@ -51,8 +51,6 @@ INT64_MIN_PLUS_1, ) -pytestmark = pytest.mark.aggregate - # Property [Int32 overflow]: $subtract promotes int32 results to int64 on overflow/underflow. INT32_OVERFLOW_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_bson_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_bson_types.py index b2f982d35..a29653bdb 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_bson_types.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_bson_types.py @@ -12,16 +12,19 @@ generate_bson_rejection_test_cases, ) -pytestmark = pytest.mark.aggregate - -_NUMERIC_TYPES = [BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL] - # Property [Minuend type acceptance]: $subtract accepts numeric and date types as minuend. # Property [Minuend type rejection]: $subtract rejects all other BSON types as minuend. SUBTRACT_MINUEND_SPEC = BsonTypeTestCase( id="subtract_minuend", msg="$subtract minuend type", - valid_types=_NUMERIC_TYPES + [BsonType.DATE, BsonType.NULL], + valid_types=[ + BsonType.DOUBLE, + BsonType.INT, + BsonType.LONG, + BsonType.DECIMAL, + BsonType.DATE, + BsonType.NULL, + ], ) # Property [Subtrahend type acceptance]: $subtract accepts numeric types as subtrahend. @@ -30,7 +33,7 @@ SUBTRACT_SUBTRAHEND_SPEC = BsonTypeTestCase( id="subtract_subtrahend", msg="$subtract subtrahend type", - valid_types=_NUMERIC_TYPES + [BsonType.NULL], + valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL, BsonType.NULL], ) MINUEND_REJECTION_CASES = generate_bson_rejection_test_cases([SUBTRACT_MINUEND_SPEC]) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py index 9c3f4373e..e388a5735 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py @@ -18,8 +18,6 @@ INT32_ZERO, ) -pytestmark = pytest.mark.aggregate - # Property [Date - numeric]: $subtract returns a date when the minuend is a date and subtrahend # is numeric (milliseconds). DATE_NUMERIC_TESTS: list[ExpressionTestCase] = [ @@ -101,7 +99,7 @@ ), ] -# Property [Date rounding]: fractional ms in the subtrahend are rounded using round-half-to-even. +# Property [Date rounding]: fractional ms operands are rounded using round-half-up. DATE_ROUNDING_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "date_decimal", @@ -111,7 +109,7 @@ }, expression={"$subtract": ["$a", "$b"]}, expected=datetime(2026, 1, 1, 0, 0, 0, 998000, tzinfo=timezone.utc), - msg="$subtract should round Decimal128 1.5 ms to 2 ms before subtracting from a date", + msg="$subtract should round Decimal128 1.5 ms up to 2 ms before subtracting", ), ExpressionTestCase( "date_double_round_up", @@ -121,7 +119,7 @@ }, expression={"$subtract": ["$a", "$b"]}, expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), - msg="$subtract should round double 2.5 ms to 3 ms (round-half-to-even) before subtracting", + msg="$subtract should round double 2.5 ms up to 3 ms before subtracting", ), ExpressionTestCase( "date_double_truncates", diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py index 7e5042751..23f44658d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_errors.py @@ -1,8 +1,6 @@ -import uuid from datetime import datetime, timezone import pytest -from bson import Binary, MaxKey, MinKey, Timestamp from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 ExpressionTestCase, @@ -21,172 +19,8 @@ DECIMAL128_NAN, FLOAT_INFINITY, FLOAT_NAN, - INT32_ZERO, ) -pytestmark = pytest.mark.aggregate - -_FIXED_UUID = uuid.UUID("550e8400-e29b-41d4-a716-446655440000") - -# Property [Type rejection]: $subtract rejects non-numeric, non-date types with TYPE_MISMATCH_ERROR. -TYPE_REJECTION_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "string_minuend", - doc={"a": "string", "b": 5}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a string minuend", - ), - ExpressionTestCase( - "boolean_minuend", - doc={"a": True, "b": 5}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a boolean minuend", - ), - ExpressionTestCase( - "array_minuend", - doc={"a": [2, 3], "b": 5}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject an array minuend", - ), - ExpressionTestCase( - "object_minuend", - doc={"a": {"x": 2}, "b": 5}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject an object minuend", - ), - ExpressionTestCase( - "empty_array_minuend", - doc={"a": [], "b": 5}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject an empty array minuend", - ), - ExpressionTestCase( - "empty_object_minuend", - doc={"a": {}, "b": 5}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject an empty object minuend", - ), - ExpressionTestCase( - "binary_minuend", - doc={"a": Binary(b"test", INT32_ZERO), "b": 5}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a binary minuend", - ), - ExpressionTestCase( - "timestamp_minuend", - doc={"a": Timestamp(1234567890, 1), "b": 5}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a Timestamp minuend", - ), - ExpressionTestCase( - "maxkey_minuend", - doc={"a": MaxKey(), "b": 5}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a MaxKey minuend", - ), - ExpressionTestCase( - "minkey_minuend", - doc={"a": MinKey(), "b": 5}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a MinKey minuend", - ), - ExpressionTestCase( - "uuid_minuend", - doc={"a": Binary.from_uuid(_FIXED_UUID), "b": 5}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a UUID minuend", - ), - # Invalid subtrahend types - ExpressionTestCase( - "string_subtrahend", - doc={"a": 10, "b": "string"}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a string subtrahend", - ), - ExpressionTestCase( - "boolean_subtrahend", - doc={"a": 10, "b": True}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a boolean subtrahend", - ), - ExpressionTestCase( - "array_subtrahend", - doc={"a": 10, "b": [2, 3]}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject an array subtrahend", - ), - ExpressionTestCase( - "object_subtrahend", - doc={"a": 10, "b": {"x": 2}}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject an object subtrahend", - ), - ExpressionTestCase( - "empty_array_subtrahend", - doc={"a": 10, "b": []}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject an empty array subtrahend", - ), - ExpressionTestCase( - "empty_object_subtrahend", - doc={"a": 10, "b": {}}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject an empty object subtrahend", - ), - ExpressionTestCase( - "binary_subtrahend", - doc={"a": 10, "b": Binary(b"test", INT32_ZERO)}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a binary subtrahend", - ), - ExpressionTestCase( - "timestamp_subtrahend", - doc={"a": 10, "b": Timestamp(1234567890, 1)}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a Timestamp subtrahend", - ), - ExpressionTestCase( - "maxkey_subtrahend", - doc={"a": 10, "b": MaxKey()}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a MaxKey subtrahend", - ), - ExpressionTestCase( - "minkey_subtrahend", - doc={"a": 10, "b": MinKey()}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a MinKey subtrahend", - ), - ExpressionTestCase( - "uuid_subtrahend", - doc={"a": 10, "b": Binary.from_uuid(_FIXED_UUID)}, - expression={"$subtract": ["$a", "$b"]}, - error_code=TYPE_MISMATCH_ERROR, - msg="$subtract should reject a UUID subtrahend", - ), -] - # Property [Date constraint]: $subtract enforces date arithmetic type rules. DATE_CONSTRAINT_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( @@ -271,7 +105,7 @@ ] SUBTRACT_ERROR_TESTS: list[ExpressionTestCase] = ( - TYPE_REJECTION_TESTS + DATE_CONSTRAINT_TESTS + DATE_NAN_INF_TESTS + ARITY_TESTS + DATE_CONSTRAINT_TESTS + DATE_NAN_INF_TESTS + ARITY_TESTS ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py index a29f8e5c6..dbb9d5c27 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_input_forms.py @@ -11,8 +11,6 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import INT32_ZERO -pytestmark = pytest.mark.aggregate - # Property [Nested expressions]: $subtract accepts other expressions as operands. NESTED_EXPRESSION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py index 20bd2024b..987660216 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_non_finite.py @@ -21,8 +21,6 @@ INT32_ZERO, ) -pytestmark = pytest.mark.aggregate - # Property [Infinity propagation]: $subtract propagates Infinity when one operand is infinite. INFINITY_PROPAGATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py index 28696146f..3b31b3bed 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_null.py @@ -10,8 +10,6 @@ from documentdb_tests.framework.parametrize import pytest_params from documentdb_tests.framework.test_constants import MISSING -pytestmark = pytest.mark.aggregate - # Property [Null propagation]: $subtract returns null when either operand is null or missing. NULL_PROPAGATION_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( From 1f8a449f9cece775a86032f12621a0f9e93cdea5 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 10 Jul 2026 15:44:28 -0700 Subject: [PATCH 10/10] Changed round wording, added negative round-down test Signed-off-by: PatersonProjects --- .../arithmetic/subtract/test_subtract_dates.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py index e388a5735..0a577fe7e 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/subtract/test_subtract_dates.py @@ -99,7 +99,7 @@ ), ] -# Property [Date rounding]: fractional ms operands are rounded using round-half-up. +# Property [Date rounding]: fractional ms operands are rounded. DATE_ROUNDING_TESTS: list[ExpressionTestCase] = [ ExpressionTestCase( "date_decimal", @@ -109,7 +109,7 @@ }, expression={"$subtract": ["$a", "$b"]}, expected=datetime(2026, 1, 1, 0, 0, 0, 998000, tzinfo=timezone.utc), - msg="$subtract should round Decimal128 1.5 ms up to 2 ms before subtracting", + msg="$subtract should round Decimal128 1.5 ms to 2 ms before subtracting", ), ExpressionTestCase( "date_double_round_up", @@ -121,6 +121,16 @@ expected=datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), msg="$subtract should round double 2.5 ms up to 3 ms before subtracting", ), + ExpressionTestCase( + "date_double_round_down_negative", + doc={ + "a": datetime(2026, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + "b": -2.5, + }, + expression={"$subtract": ["$a", "$b"]}, + expected=datetime(2026, 1, 1, 0, 0, 0, 3000, tzinfo=timezone.utc), + msg="$subtract should round double -2.5 ms down to -3 ms before subtracting", + ), ExpressionTestCase( "date_double_truncates", doc={