From 51a482ab4db4faa743125c3ac8699d126a8fbf33 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 13:32:35 -0700 Subject: [PATCH 1/4] 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 2/4] 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 8c24aa70385bd96f5c12e6e8f6f0ff2bd7e19cd3 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 15:46:16 -0700 Subject: [PATCH 3/4] Added tests Signed-off-by: PatersonProjects --- .../arithmetic/add/test_add_errors.py | 65 +++-- .../arithmetic/ceil/test_ceil_boundaries.py | 237 ++++++++++++++++++ .../arithmetic/ceil/test_ceil_errors.py | 89 +++++++ .../arithmetic/ceil/test_ceil_input_forms.py | 76 ++++++ .../arithmetic/ceil/test_ceil_non_finite.py | 87 +++++++ .../arithmetic/ceil/test_ceil_null.py | 45 ++++ .../arithmetic/ceil/test_ceil_numeric.py | 162 ++++++++++++ .../arithmetic/ceil/test_ceil_return_type.py | 57 +++++ 8 files changed, 791 insertions(+), 27 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_boundaries.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_input_forms.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_non_finite.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_null.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_numeric.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_return_type.py 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..8e319d956 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 @@ -3,7 +3,6 @@ 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, @@ -12,6 +11,12 @@ assert_expression_result, execute_expression_with_insert, ) +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_rejection_test_cases, +) from documentdb_tests.framework.error_codes import ( MORE_THAN_ONE_DATE_ERROR, OVERFLOW_ERROR, @@ -26,29 +31,36 @@ INT64_MAX, ) +_NUMERIC_DATE_AND_NULL_TYPES = [ + BsonType.DOUBLE, + BsonType.INT, + BsonType.LONG, + BsonType.DECIMAL, + BsonType.DATE, + BsonType.NULL, +] + # 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", +ADD_TYPE_SPEC = BsonTypeTestCase( + id="add_operand", + msg="$add should reject non-numeric, non-date operand", + keyword="$add", + valid_types=_NUMERIC_DATE_AND_NULL_TYPES, + default_error_code=TYPE_MISMATCH_ERROR, +) + +ADD_TYPE_REJECTION_CASES = generate_bson_rejection_test_cases([ADD_TYPE_SPEC]) +_ADD_TYPE_REJECTION_EXPR = {"$add": [1, "$b"]} + + +@pytest.mark.parametrize("bson_type,sample_value,spec", ADD_TYPE_REJECTION_CASES) +def test_add_rejects_invalid_operand_type(collection, bson_type, sample_value, spec): + """Test $add rejects non-numeric, non-date BSON types as operands.""" + result = execute_expression_with_insert( + collection, _ADD_TYPE_REJECTION_EXPR, {"b": sample_value} ) - 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(){}")), - ] -] + assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) + # Property [Mixed Valid and Invalid]: $add rejects an invalid operand when it appears among # valid numeric operands. @@ -183,9 +195,8 @@ ), ] -ADD_ERROR_ALL_TESTS = ( - ADD_TYPE_ERROR_TESTS - + ADD_MIXED_VALID_INVALID_TESTS +ADD_REMAINING_ERROR_TESTS = ( + ADD_MIXED_VALID_INVALID_TESTS + ADD_SINGLE_TYPE_ERROR_TESTS + ADD_MULTIPLE_DATE_TESTS + ADD_DATE_NON_FINITE_ERROR_TESTS @@ -193,9 +204,9 @@ ) -@pytest.mark.parametrize("test_case", pytest_params(ADD_ERROR_ALL_TESTS)) +@pytest.mark.parametrize("test_case", pytest_params(ADD_REMAINING_ERROR_TESTS)) def test_add_errors(collection, test_case: ExpressionTestCase): - """Test $add type, multiple-date, and date non-finite error cases.""" + """Test $add multiple-date, single-invalid, and date non-finite error cases.""" result = execute_expression_with_insert(collection, test_case.expression, test_case.doc) assert_expression_result( result, diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_boundaries.py new file mode 100644 index 000000000..151f42977 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_boundaries.py @@ -0,0 +1,237 @@ +"""Tests for $ceil at representable-range boundaries for int32, int64, double, and decimal128.""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_JUST_ABOVE_HALF, + DECIMAL128_JUST_BELOW_HALF, + DECIMAL128_LARGE_EXPONENT, + DECIMAL128_MANY_TRAILING_ZEROS, + DECIMAL128_MAX, + DECIMAL128_MIN, + DECIMAL128_NAN, + DECIMAL128_SMALL_EXPONENT, + DECIMAL128_TRAILING_ZERO, + DOUBLE_JUST_BELOW_HALF, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_MIN_NEGATIVE_SUBNORMAL, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_PRECISION_LOSS, + INT32_MAX, + INT32_MIN, + INT32_OVERFLOW, + INT32_UNDERFLOW, + INT64_MAX, + INT64_MAX_MINUS_1, + INT64_MIN, + INT64_MIN_PLUS_1, +) + +# Property [Int32 Boundaries]: $ceil of an integer returns the same integer unchanged. +CEIL_INT32_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_max", + doc={"value": INT32_MAX}, + expression={"$ceil": ["$value"]}, + expected=INT32_MAX, + msg="$ceil should return INT32_MAX unchanged", + ), + ExpressionTestCase( + "int32_min", + doc={"value": INT32_MIN}, + expression={"$ceil": ["$value"]}, + expected=INT32_MIN, + msg="$ceil should return INT32_MIN unchanged", + ), + ExpressionTestCase( + "int32_overflow", + doc={"value": INT32_OVERFLOW}, + expression={"$ceil": ["$value"]}, + expected=Int64(INT32_OVERFLOW), + msg="$ceil should return INT32_OVERFLOW unchanged as int64", + ), + ExpressionTestCase( + "int32_underflow", + doc={"value": INT32_UNDERFLOW}, + expression={"$ceil": ["$value"]}, + expected=Int64(INT32_UNDERFLOW), + msg="$ceil should return INT32_UNDERFLOW unchanged as int64", + ), +] + +# Property [Int64 Boundaries]: $ceil of an int64 integer returns the same int64 unchanged. +CEIL_INT64_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int64_max", + doc={"value": INT64_MAX}, + expression={"$ceil": ["$value"]}, + expected=INT64_MAX, + msg="$ceil should return INT64_MAX unchanged", + ), + ExpressionTestCase( + "int64_max_minus_1", + doc={"value": INT64_MAX_MINUS_1}, + expression={"$ceil": ["$value"]}, + expected=INT64_MAX_MINUS_1, + msg="$ceil should return INT64_MAX-1 unchanged", + ), + ExpressionTestCase( + "int64_min", + doc={"value": INT64_MIN}, + expression={"$ceil": ["$value"]}, + expected=INT64_MIN, + msg="$ceil should return INT64_MIN unchanged", + ), + ExpressionTestCase( + "int64_min_plus_1", + doc={"value": INT64_MIN_PLUS_1}, + expression={"$ceil": ["$value"]}, + expected=INT64_MIN_PLUS_1, + msg="$ceil should return INT64_MIN+1 unchanged", + ), +] + +# Property [Double Boundaries]: $ceil rounds double boundary values up to the nearest integer. +CEIL_DOUBLE_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_min_subnormal", + doc={"value": DOUBLE_MIN_SUBNORMAL}, + expression={"$ceil": ["$value"]}, + expected=1.0, + msg="$ceil should round the minimum subnormal double up to 1.0", + ), + ExpressionTestCase( + "double_min_negative_subnormal", + doc={"value": DOUBLE_MIN_NEGATIVE_SUBNORMAL}, + expression={"$ceil": ["$value"]}, + expected=DOUBLE_NEGATIVE_ZERO, + msg="$ceil should round the minimum negative subnormal double up to negative zero", + ), + ExpressionTestCase( + "double_near_min", + doc={"value": DOUBLE_NEAR_MIN}, + expression={"$ceil": ["$value"]}, + expected=1.0, + msg="$ceil should round a near-min double up to 1.0", + ), + ExpressionTestCase( + "double_near_max", + doc={"value": DOUBLE_NEAR_MAX}, + expression={"$ceil": ["$value"]}, + expected=DOUBLE_NEAR_MAX, + msg="$ceil should return the same value for a near-max double", + ), + ExpressionTestCase( + "double_max_safe_integer", + doc={"value": float(DOUBLE_MAX_SAFE_INTEGER)}, + expression={"$ceil": ["$value"]}, + expected=float(DOUBLE_MAX_SAFE_INTEGER), + msg="$ceil should return the max safe integer double unchanged", + ), + ExpressionTestCase( + "double_precision_loss", + doc={"value": float(DOUBLE_PRECISION_LOSS)}, + expression={"$ceil": ["$value"]}, + expected=float(DOUBLE_PRECISION_LOSS), + msg="$ceil should return the precision loss double unchanged", + ), + ExpressionTestCase( + "double_fraction_between_zero_and_one", + doc={"value": DOUBLE_JUST_BELOW_HALF}, + expression={"$ceil": ["$value"]}, + expected=1.0, + msg="$ceil should round a double fraction in (0,1) up to 1.0", + ), +] + +# Property [Decimal128 Boundaries]: $ceil rounds decimal128 boundary values up to the nearest +# integer, returning NaN when the value overflows the representable range. +CEIL_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal128_max", + doc={"value": DECIMAL128_MAX}, + expression={"$ceil": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$ceil should return NaN for decimal128 max (overflow)", + ), + ExpressionTestCase( + "decimal128_min", + doc={"value": DECIMAL128_MIN}, + expression={"$ceil": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$ceil should return NaN for decimal128 min (overflow)", + ), + ExpressionTestCase( + "decimal128_large_exponent", + doc={"value": DECIMAL128_LARGE_EXPONENT}, + expression={"$ceil": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$ceil should return NaN for a decimal128 with a large exponent (overflow)", + ), + ExpressionTestCase( + "decimal128_small_exponent", + doc={"value": DECIMAL128_SMALL_EXPONENT}, + expression={"$ceil": ["$value"]}, + expected=Decimal128("1"), + msg="$ceil should round a decimal128 with a small exponent up to 1", + ), + ExpressionTestCase( + "decimal128_trailing_zero", + doc={"value": DECIMAL128_TRAILING_ZERO}, + expression={"$ceil": ["$value"]}, + expected=Decimal128("1"), + msg="$ceil should round a decimal128 with a trailing zero up to 1", + ), + ExpressionTestCase( + "decimal128_many_trailing_zeros", + doc={"value": DECIMAL128_MANY_TRAILING_ZEROS}, + expression={"$ceil": ["$value"]}, + expected=Decimal128("1"), + msg="$ceil should round a decimal128 with many trailing zeros up to 1", + ), + ExpressionTestCase( + "decimal128_just_below_half", + doc={"value": DECIMAL128_JUST_BELOW_HALF}, + expression={"$ceil": ["$value"]}, + expected=Decimal128("1"), + msg="$ceil should round a decimal128 just below half up to 1", + ), + ExpressionTestCase( + "decimal128_just_above_half", + doc={"value": DECIMAL128_JUST_ABOVE_HALF}, + expression={"$ceil": ["$value"]}, + expected=Decimal128("1"), + msg="$ceil should round a decimal128 just above half up to 1", + ), +] + +CEIL_BOUNDARY_ALL_TESTS = ( + CEIL_INT32_BOUNDARY_TESTS + + CEIL_INT64_BOUNDARY_TESTS + + CEIL_DOUBLE_BOUNDARY_TESTS + + CEIL_DECIMAL_BOUNDARY_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(CEIL_BOUNDARY_ALL_TESTS)) +def test_ceil_boundaries(collection, test_case: ExpressionTestCase): + """Test $ceil representable-range boundary cases for all numeric types.""" + 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/ceil/test_ceil_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_errors.py new file mode 100644 index 000000000..7ce6b9b19 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_errors.py @@ -0,0 +1,89 @@ +"""Tests for $ceil type mismatch and arity error cases.""" + +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.assertions import assertFailureCode +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import ( + EXPRESSION_TYPE_MISMATCH_ERROR, + NON_NUMERIC_TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params + +_NUMERIC_AND_NULL_TYPES = [ + BsonType.DOUBLE, + BsonType.INT, + BsonType.LONG, + BsonType.DECIMAL, + BsonType.NULL, +] + +# Property [Type Strictness]: $ceil rejects all non-numeric, non-null BSON types. +CEIL_TYPE_SPEC = BsonTypeTestCase( + id="ceil_input", + msg="$ceil should reject non-numeric input", + keyword="$ceil", + valid_types=_NUMERIC_AND_NULL_TYPES, + default_error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, +) + +CEIL_TYPE_REJECTION_CASES = generate_bson_rejection_test_cases([CEIL_TYPE_SPEC]) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", CEIL_TYPE_REJECTION_CASES) +def test_ceil_rejects_non_numeric_input(collection, bson_type, sample_value, spec): + """Test $ceil rejects non-numeric BSON types.""" + result = execute_expression_with_insert( + collection, {"$ceil": ["$value"]}, {"value": sample_value} + ) + assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) + + +# Property [Array Input]: $ceil rejects array-typed input; it does not apply element-wise. +# Property [Arity]: $ceil requires exactly one argument. +CEIL_ARGUMENT_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "composite_array_field_path", + doc={"a": [{"b": 1.5}]}, + expression={"$ceil": "$a.b"}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$ceil should reject a field path that resolves to an array", + ), + ExpressionTestCase( + "arity_zero", + doc={}, + expression={"$ceil": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$ceil should reject zero arguments", + ), + ExpressionTestCase( + "arity_two", + doc={}, + expression={"$ceil": [1, 2]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$ceil should reject two arguments", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(CEIL_ARGUMENT_ERROR_TESTS)) +def test_ceil_argument_errors(collection, test_case: ExpressionTestCase): + """Test $ceil rejects invalid argument count and array-typed input.""" + 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/ceil/test_ceil_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_input_forms.py new file mode 100644 index 000000000..081deadd6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_input_forms.py @@ -0,0 +1,76 @@ +"""Tests for $ceil argument forms: array-wrapped, bare, literal, and nested expression inputs.""" + +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 [Argument Form]: $ceil accepts its single argument bare or wrapped in a one-element +# array. +CEIL_ARGUMENT_FORM_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "form_array", + doc={"value": 1.5}, + expression={"$ceil": ["$value"]}, + expected=2.0, + msg="$ceil should accept its argument wrapped in a one-element array", + ), + ExpressionTestCase( + "form_bare", + doc={"value": 1.5}, + expression={"$ceil": "$value"}, + expected=2.0, + msg="$ceil should accept its argument without an array wrapper", + ), +] + +# Property [Literal Input]: $ceil evaluates an inline literal argument, not only document fields. +CEIL_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_input", + doc={}, + expression={"$ceil": [4.1]}, + expected=5.0, + msg="$ceil should return the ceiling of an inline literal argument", + ), +] + +# Property [Expression Input]: $ceil evaluates a nested expression argument before rounding up. +CEIL_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_ceil", + doc={"value": 4.1}, + expression={"$ceil": {"$ceil": "$value"}}, + expected=5.0, + msg="$ceil should evaluate a nested $ceil expression argument", + ), + ExpressionTestCase( + "object_expression_input", + doc={"value": 1.5}, + expression={"$ceil": {"$multiply": ["$value", 1.0]}}, + expected=2.0, + msg="$ceil should accept an object expression as its argument", + ), +] + +CEIL_INPUT_FORM_ALL_TESTS = ( + CEIL_ARGUMENT_FORM_TESTS + CEIL_LITERAL_TESTS + CEIL_EXPRESSION_INPUT_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(CEIL_INPUT_FORM_ALL_TESTS)) +def test_ceil_input_forms(collection, test_case: ExpressionTestCase): + """Test $ceil argument form, literal, and nested expression input 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/ceil/test_ceil_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_non_finite.py new file mode 100644 index 000000000..3a8f0c8dc --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_non_finite.py @@ -0,0 +1,87 @@ +"""Tests for $ceil with infinity and NaN inputs for double and decimal128 types.""" + +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]: $ceil of positive or negative float infinity returns the same infinity. +# $ceil of decimal128 infinity returns NaN (overflow during rounding). +CEIL_INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "float_infinity", + doc={"value": FLOAT_INFINITY}, + expression={"$ceil": ["$value"]}, + expected=FLOAT_INFINITY, + msg="$ceil should return float infinity for float infinity", + ), + ExpressionTestCase( + "float_negative_infinity", + doc={"value": FLOAT_NEGATIVE_INFINITY}, + expression={"$ceil": ["$value"]}, + expected=FLOAT_NEGATIVE_INFINITY, + msg="$ceil should return float negative infinity for float negative infinity", + ), + ExpressionTestCase( + "decimal128_infinity", + doc={"value": DECIMAL128_INFINITY}, + expression={"$ceil": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$ceil should return NaN for decimal128 infinity", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + doc={"value": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$ceil": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$ceil should return NaN for decimal128 negative infinity", + ), +] + +# Property [NaN]: $ceil of NaN returns NaN of the same type. +CEIL_NAN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "float_nan", + doc={"value": FLOAT_NAN}, + expression={"$ceil": ["$value"]}, + expected=pytest.approx(math.nan, nan_ok=True), + msg="$ceil should return NaN for float NaN", + ), + ExpressionTestCase( + "decimal128_nan", + doc={"value": DECIMAL128_NAN}, + expression={"$ceil": ["$value"]}, + expected=DECIMAL128_NAN, + msg="$ceil should return NaN for decimal128 NaN", + ), +] + +CEIL_NON_FINITE_TESTS = CEIL_INFINITY_TESTS + CEIL_NAN_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(CEIL_NON_FINITE_TESTS)) +def test_ceil_non_finite(collection, test_case: ExpressionTestCase): + """Test $ceil infinity and NaN cases for double and decimal128.""" + 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/ceil/test_ceil_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_null.py new file mode 100644 index 000000000..ef29f5126 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_null.py @@ -0,0 +1,45 @@ +"""Tests for $ceil null and missing field propagation.""" + +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]: $ceil of null or a missing field returns null. +CEIL_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_value", + doc={"value": None}, + expression={"$ceil": ["$value"]}, + expected=None, + msg="$ceil should return null for null input", + ), + ExpressionTestCase( + "missing_field", + doc={}, + expression={"$ceil": [MISSING]}, + expected=None, + msg="$ceil should return null for a missing field", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(CEIL_NULL_TESTS)) +def test_ceil_null(collection, test_case: ExpressionTestCase): + """Test $ceil 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/ceil/test_ceil_numeric.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_numeric.py new file mode 100644 index 000000000..58d709142 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_numeric.py @@ -0,0 +1,162 @@ +"""Tests for $ceil basic numeric rounding across int32, int64, double, and decimal128 types.""" + +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_NEGATIVE_ZERO, + DOUBLE_NEGATIVE_ZERO, +) + +# Property [Integer Identity]: $ceil of any integer value returns the same integer unchanged. +CEIL_INTEGER_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "positive_int32", + doc={"value": 1}, + expression={"$ceil": ["$value"]}, + expected=1, + msg="$ceil should return the same value for a positive int32", + ), + ExpressionTestCase( + "negative_int32", + doc={"value": -1}, + expression={"$ceil": ["$value"]}, + expected=-1, + msg="$ceil should return the same value for a negative int32", + ), + ExpressionTestCase( + "zero_int32", + doc={"value": 0}, + expression={"$ceil": ["$value"]}, + expected=0, + msg="$ceil should return zero for int32 zero", + ), + ExpressionTestCase( + "positive_int64", + doc={"value": Int64(1)}, + expression={"$ceil": ["$value"]}, + expected=Int64(1), + msg="$ceil should return the same value for a positive int64", + ), + ExpressionTestCase( + "negative_int64", + doc={"value": Int64(-1)}, + expression={"$ceil": ["$value"]}, + expected=Int64(-1), + msg="$ceil should return the same value for a negative int64", + ), + ExpressionTestCase( + "zero_int64", + doc={"value": Int64(0)}, + expression={"$ceil": ["$value"]}, + expected=Int64(0), + msg="$ceil should return zero for int64 zero", + ), +] + +# Property [Double Rounding]: $ceil rounds a double up to the nearest integer double. +CEIL_DOUBLE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "positive_double", + doc={"value": 1.5}, + expression={"$ceil": ["$value"]}, + expected=2.0, + msg="$ceil should round up a positive double to the next integer", + ), + ExpressionTestCase( + "negative_double", + doc={"value": -1.5}, + expression={"$ceil": ["$value"]}, + expected=-1.0, + msg="$ceil should round a negative double toward zero", + ), + ExpressionTestCase( + "zero_double", + doc={"value": 0.0}, + expression={"$ceil": ["$value"]}, + expected=0.0, + msg="$ceil should return zero for double zero", + ), + ExpressionTestCase( + "negative_zero_double", + doc={"value": DOUBLE_NEGATIVE_ZERO}, + expression={"$ceil": ["$value"]}, + expected=DOUBLE_NEGATIVE_ZERO, + msg="$ceil should return zero for negative zero double", + ), + ExpressionTestCase( + "negative_fraction", + doc={"value": -0.5}, + expression={"$ceil": ["$value"]}, + expected=-0.0, + msg="$ceil should round a negative double fraction up to negative zero", + ), +] + +# Property [Decimal128 Rounding]: $ceil rounds a decimal128 up to the nearest integer decimal128. +CEIL_DECIMAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "positive_decimal", + doc={"value": Decimal128("1")}, + expression={"$ceil": ["$value"]}, + expected=Decimal128("1"), + msg="$ceil should return the same value for a positive whole decimal128", + ), + ExpressionTestCase( + "negative_decimal", + doc={"value": Decimal128("-1")}, + expression={"$ceil": ["$value"]}, + expected=Decimal128("-1"), + msg="$ceil should return the same value for a negative whole decimal128", + ), + ExpressionTestCase( + "zero_decimal", + doc={"value": Decimal128("0")}, + expression={"$ceil": ["$value"]}, + expected=Decimal128("0"), + msg="$ceil should return zero for decimal128 zero", + ), + ExpressionTestCase( + "negative_zero_decimal", + doc={"value": DECIMAL128_NEGATIVE_ZERO}, + expression={"$ceil": ["$value"]}, + expected=DECIMAL128_NEGATIVE_ZERO, + msg="$ceil should return negative zero for negative zero decimal128", + ), + ExpressionTestCase( + "positive_decimal_fraction", + doc={"value": Decimal128("1.5")}, + expression={"$ceil": ["$value"]}, + expected=Decimal128("2"), + msg="$ceil should round up a positive decimal128 fraction", + ), + ExpressionTestCase( + "negative_decimal_fraction", + doc={"value": Decimal128("-1.5")}, + expression={"$ceil": ["$value"]}, + expected=Decimal128("-1"), + msg="$ceil should round a negative decimal128 fraction toward zero", + ), +] + +CEIL_NUMERIC_ALL_TESTS = CEIL_INTEGER_TESTS + CEIL_DOUBLE_TESTS + CEIL_DECIMAL_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(CEIL_NUMERIC_ALL_TESTS)) +def test_ceil_numeric(collection, test_case: ExpressionTestCase): + """Test $ceil basic numeric rounding for integer, double, and decimal128 inputs.""" + 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/ceil/test_ceil_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_return_type.py new file mode 100644 index 000000000..9d46d3ec6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/test_ceil_return_type.py @@ -0,0 +1,57 @@ +"""Tests for $ceil return type preservation across numeric types.""" + +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]: $ceil preserves the numeric type of its input. +CEIL_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type_int32", + doc={"value": 5}, + expression={"$type": {"$ceil": "$value"}}, + expected="int", + msg="$ceil should preserve int32 type", + ), + ExpressionTestCase( + "return_type_int64", + doc={"value": Int64(5)}, + expression={"$type": {"$ceil": "$value"}}, + expected="long", + msg="$ceil should preserve int64 type", + ), + ExpressionTestCase( + "return_type_double", + doc={"value": 1.5}, + expression={"$type": {"$ceil": "$value"}}, + expected="double", + msg="$ceil should preserve double type", + ), + ExpressionTestCase( + "return_type_decimal", + doc={"value": Decimal128("1.5")}, + expression={"$type": {"$ceil": "$value"}}, + expected="decimal", + msg="$ceil should preserve decimal128 type", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(CEIL_RETURN_TYPE_TESTS)) +def test_ceil_return_type(collection, test_case: ExpressionTestCase): + """Test $ceil return type preservation 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, + ) From 7a6a093dce6c9edae31d98e03949664818d1558c Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 30 Jun 2026 16:01:40 -0700 Subject: [PATCH 4/4] Added init files Signed-off-by: PatersonProjects --- .../tests/core/operator/expressions/arithmetic/add/__init__.py | 0 .../tests/core/operator/expressions/arithmetic/ceil/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/__init__.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/add/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ceil/__init__.py new file mode 100644 index 000000000..e69de29bb