diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_boundaries.py new file mode 100644 index 000000000..797b82f68 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_boundaries.py @@ -0,0 +1,158 @@ +"""Tests for $ln at representable-range boundaries, including subnormal and extreme inputs.""" + +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_MAX, + DECIMAL128_MIN_POSITIVE, + DECIMAL128_SMALL_EXPONENT, + DECIMAL128_TRAILING_ZERO, + DECIMAL128_ZERO, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + DOUBLE_PRECISION_LOSS, + INT32_MAX, + INT32_MAX_MINUS_1, + INT64_MAX, + INT64_MAX_MINUS_1, +) + +# Property [Integer Boundaries]: $ln of the largest representable integers returns a finite double. +LN_INTEGER_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_max", + doc={"value": INT32_MAX}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(21.487562596892644), + msg="$ln should return the natural log of INT32_MAX", + ), + ExpressionTestCase( + "int32_max_minus_1", + doc={"value": INT32_MAX_MINUS_1}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(21.487562596426983), + msg="$ln should return the natural log of INT32_MAX minus one", + ), + ExpressionTestCase( + "int64_max", + doc={"value": INT64_MAX}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(43.66827237527655), + msg="$ln should return the natural log of INT64_MAX", + ), + ExpressionTestCase( + "int64_max_minus_1", + doc={"value": INT64_MAX_MINUS_1}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(43.66827237527655), + msg="$ln should return the natural log of INT64_MAX minus one", + ), +] + +# Property [Double Boundaries]: $ln at the double subnormal and near-limit range returns the +# expected large-magnitude values. +LN_DOUBLE_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_min_subnormal", + doc={"value": DOUBLE_MIN_SUBNORMAL}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(-744.4400719213812), + msg="$ln should return a large negative value for the minimum subnormal double", + ), + ExpressionTestCase( + "double_near_min", + doc={"value": DOUBLE_NEAR_MIN}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(-709.1962086421661), + msg="$ln should return a large negative value for a near-zero positive double", + ), + ExpressionTestCase( + "double_near_max", + doc={"value": DOUBLE_NEAR_MAX}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(709.1962086421661), + msg="$ln should return a large positive value for a near-maximum double", + ), + ExpressionTestCase( + "double_max_safe_integer", + doc={"value": DOUBLE_MAX_SAFE_INTEGER}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(36.7368005696771), + msg="$ln should return the natural log of the maximum safe integer double", + ), + ExpressionTestCase( + "double_precision_loss", + doc={"value": DOUBLE_PRECISION_LOSS}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(36.7368005696771), + msg="$ln should return the same value as the maximum safe integer for the next " + "integer double, which is not representable", + ), + ExpressionTestCase( + "very_small_positive", + doc={"value": 1e-300}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(-690.7755278982137), + msg="$ln should return a large negative value for a very small positive double", + ), +] + +# Property [Decimal128 Boundaries]: $ln of extreme decimal128 exponents returns a full-precision +# decimal128 result. +LN_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_small_exponent", + doc={"value": DECIMAL128_SMALL_EXPONENT}, + expression={"$ln": ["$value"]}, + expected=Decimal128("-14144.78022626242263692252150612605"), + msg="$ln should return a large negative decimal128 for a decimal128 with a small exponent", + ), + ExpressionTestCase( + "decimal_min_positive", + doc={"value": DECIMAL128_MIN_POSITIVE}, + expression={"$ln": ["$value"]}, + expected=Decimal128("-14220.76553433122614449511522413063"), + msg="$ln should return a large negative decimal128 for the smallest positive decimal128", + ), + ExpressionTestCase( + "decimal_max", + doc={"value": DECIMAL128_MAX}, + expression={"$ln": ["$value"]}, + expected=Decimal128("14149.38539644841072829055748903542"), + msg="$ln should return a large positive decimal128 for the maximum decimal128", + ), + ExpressionTestCase( + "decimal_trailing_zero", + doc={"value": DECIMAL128_TRAILING_ZERO}, + expression={"$ln": ["$value"]}, + expected=DECIMAL128_ZERO, + msg="$ln should return zero for a decimal128 one with a trailing zero", + ), +] + +LN_BOUNDARY_ALL_TESTS = ( + LN_INTEGER_BOUNDARY_TESTS + LN_DOUBLE_BOUNDARY_TESTS + LN_DECIMAL_BOUNDARY_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(LN_BOUNDARY_ALL_TESTS)) +def test_ln_boundaries(collection, test_case: ExpressionTestCase): + """Test $ln representable-range boundary 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/ln/test_ln_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_errors.py new file mode 100644 index 000000000..1ea2c103d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_errors.py @@ -0,0 +1,254 @@ +"""Tests for $ln domain, type, and arity errors.""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, 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 ( + EXPRESSION_TYPE_MISMATCH_ERROR, + LN_NON_POSITIVE_INPUT_ERROR, + NON_NUMERIC_TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_MAX_NEGATIVE, + DECIMAL128_MIN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ZERO, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_ZERO, + FLOAT_NEGATIVE_INFINITY, + INT32_MIN, + INT64_MIN, +) + +# Property [Domain]: $ln rejects non-positive inputs, including zero, negative zero, negative +# values, and negative infinity. +LN_DOMAIN_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "zero_int32", + doc={"value": 0}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject int32 zero", + ), + ExpressionTestCase( + "zero_double", + doc={"value": DOUBLE_ZERO}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject double zero", + ), + ExpressionTestCase( + "zero_decimal", + doc={"value": DECIMAL128_ZERO}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject decimal128 zero", + ), + ExpressionTestCase( + "negative_zero_double", + doc={"value": DOUBLE_NEGATIVE_ZERO}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject negative zero double", + ), + ExpressionTestCase( + "negative_zero_decimal", + doc={"value": DECIMAL128_NEGATIVE_ZERO}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject negative zero decimal128", + ), + ExpressionTestCase( + "negative_int32", + doc={"value": -1}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject a negative integer", + ), + ExpressionTestCase( + "negative_ten", + doc={"value": -10}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject negative ten", + ), + ExpressionTestCase( + "negative_double", + doc={"value": -0.5}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject a negative double", + ), + ExpressionTestCase( + "negative_decimal", + doc={"value": Decimal128("-1")}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject a negative decimal128", + ), + ExpressionTestCase( + "int32_min", + doc={"value": INT32_MIN}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject INT32_MIN", + ), + ExpressionTestCase( + "int64_min", + doc={"value": INT64_MIN}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject INT64_MIN", + ), + ExpressionTestCase( + "decimal_min", + doc={"value": DECIMAL128_MIN}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject the minimum decimal128", + ), + ExpressionTestCase( + "decimal_max_negative", + doc={"value": DECIMAL128_MAX_NEGATIVE}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject the smallest-magnitude negative decimal128", + ), + ExpressionTestCase( + "float_negative_infinity", + doc={"value": FLOAT_NEGATIVE_INFINITY}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject float negative infinity", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + doc={"value": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$ln": ["$value"]}, + error_code=LN_NON_POSITIVE_INPUT_ERROR, + msg="$ln should reject decimal128 negative infinity", + ), +] + +# Property [Type Strictness]: $ln rejects non-numeric input types. +LN_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + *[ + ExpressionTestCase( + f"type_{tid}", + doc={"value": val}, + expression={"$ln": ["$value"]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg=f"$ln should reject a {tid} input", + ) + for tid, val in [ + ("string", "abc"), + ("bool", True), + ("array", [1, 2]), + ("object", {"a": 1}), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("regex", Regex("abc")), + ("binary", Binary(b"data")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("timestamp", Timestamp(1, 1)), + ("code", Code("function(){}")), + ] + ], + ExpressionTestCase( + "empty_array", + doc={"value": []}, + expression={"$ln": ["$value"]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$ln should reject an empty array input", + ), + ExpressionTestCase( + "empty_object", + doc={"value": {}}, + expression={"$ln": ["$value"]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$ln should reject an empty object input", + ), +] + +# Property [Non-Numeric Expression and Path Inputs]: array and object expression inputs, and field +# paths that resolve to an array, are delivered to $ln as values and rejected as non-numeric. +LN_EXPRESSION_INPUT_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "array_expression_input", + doc={"value": 10}, + expression={"$ln": [["$value"]]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$ln should reject an array expression input without flattening it into arguments", + ), + ExpressionTestCase( + "object_expression_input", + doc={"value": 10}, + expression={"$ln": {"z": "$value"}}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$ln should reject an object expression input as a value rather than evaluating it", + ), + ExpressionTestCase( + "array_of_objects_path", + doc={"a": [{"b": 10}, {"b": 100}]}, + expression={"$ln": "$a.b"}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$ln should reject a field path that resolves to an array from an array of objects", + ), + ExpressionTestCase( + "array_index_path", + doc={"a": [{"b": 10}, {"b": 100}]}, + expression={"$ln": "$a.0.b"}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$ln should reject an array-index field path that resolves to an array in an " + "aggregation expression", + ), +] + +# Property [Arity]: $ln requires exactly one argument. +LN_ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "arity_zero", + doc={}, + expression={"$ln": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$ln should reject zero arguments", + ), + ExpressionTestCase( + "arity_two", + doc={}, + expression={"$ln": [1, 2]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$ln should reject two arguments", + ), +] + +LN_ERROR_ALL_TESTS = ( + LN_DOMAIN_ERROR_TESTS + + LN_TYPE_ERROR_TESTS + + LN_EXPRESSION_INPUT_ERROR_TESTS + + LN_ARITY_ERROR_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(LN_ERROR_ALL_TESTS)) +def test_ln_errors(collection, test_case: ExpressionTestCase): + """Test $ln domain, type, and arity 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/ln/test_ln_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_input_forms.py new file mode 100644 index 000000000..e2ca6ae13 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_input_forms.py @@ -0,0 +1,67 @@ +"""Tests for $ln argument forms, literal input, and nested expression input.""" + +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 DOUBLE_ZERO + +# Property [Argument Form]: $ln accepts its single argument bare or wrapped in a one-element array. +LN_ARGUMENT_FORM_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "form_array", + doc={"value": 1}, + expression={"$ln": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$ln should accept its argument wrapped in a one-element array", + ), + ExpressionTestCase( + "form_bare", + doc={"value": 1}, + expression={"$ln": "$value"}, + expected=DOUBLE_ZERO, + msg="$ln should accept its argument without an array wrapper", + ), +] + +# Property [Literal Input]: $ln evaluates an inline literal argument, not only document fields. +LN_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_input", + doc={}, + expression={"$ln": [1]}, + expected=DOUBLE_ZERO, + msg="$ln should return zero for an inline literal one", + ), +] + +# Property [Expression Input]: $ln evaluates a nested expression argument before taking the log. +LN_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_exp", + doc={}, + expression={"$ln": {"$exp": 1}}, + expected=pytest.approx(1.0), + msg="$ln should evaluate a nested $exp expression argument", + ), +] + +LN_INPUT_FORM_TESTS = LN_ARGUMENT_FORM_TESTS + LN_LITERAL_TESTS + LN_EXPRESSION_INPUT_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(LN_INPUT_FORM_TESTS)) +def test_ln_input_forms(collection, test_case: ExpressionTestCase): + """Test $ln 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/ln/test_ln_magnitude.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_magnitude.py new file mode 100644 index 000000000..dad1be369 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_magnitude.py @@ -0,0 +1,217 @@ +"""Tests for $ln core natural-logarithm values across sign and numeric type.""" + +import math + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import DECIMAL128_ZERO, DOUBLE_ZERO + +# Property [Identity]: $ln of one is zero for every numeric type. +LN_ONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "one_int32", + doc={"value": 1}, + expression={"$ln": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$ln should return zero for int32 one", + ), + ExpressionTestCase( + "one_int64", + doc={"value": Int64(1)}, + expression={"$ln": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$ln should return zero for int64 one", + ), + ExpressionTestCase( + "one_double", + doc={"value": 1.0}, + expression={"$ln": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$ln should return zero for double one", + ), + ExpressionTestCase( + "one_decimal", + doc={"value": Decimal128("1")}, + expression={"$ln": ["$value"]}, + expected=DECIMAL128_ZERO, + msg="$ln should return zero for decimal128 one", + ), +] + +# Property [Value Above One]: $ln returns a positive natural logarithm for inputs greater than one. +LN_ABOVE_ONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "e_double", + doc={"value": math.e}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(1.0), + msg="$ln should return one for e", + ), + ExpressionTestCase( + "e_squared", + doc={"value": math.e**2}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(2.0), + msg="$ln should return two for e squared", + ), + ExpressionTestCase( + "two", + doc={"value": 2}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(0.6931471805599453), + msg="$ln should return the natural log of two", + ), + ExpressionTestCase( + "ten", + doc={"value": 10}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(2.302585092994046), + msg="$ln should return the natural log of ten", + ), + ExpressionTestCase( + "hundred", + doc={"value": 100}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(4.605170185988092), + msg="$ln should return the natural log of one hundred", + ), + ExpressionTestCase( + "thousand", + doc={"value": 1000}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(6.907755278982137), + msg="$ln should return the natural log of one thousand", + ), + ExpressionTestCase( + "million", + doc={"value": 1_000_000}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(13.815510557964274), + msg="$ln should return the natural log of one million", + ), + ExpressionTestCase( + "billion", + doc={"value": Int64(1_000_000_000)}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(20.72326583694641), + msg="$ln should return the natural log of one billion int64", + ), + ExpressionTestCase( + "one_and_half", + doc={"value": 1.5}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(0.4054651081081644), + msg="$ln should return the natural log of one and a half", + ), + ExpressionTestCase( + "two_and_half", + doc={"value": 2.5}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(0.9162907318741551), + msg="$ln should return the natural log of two and a half", + ), + ExpressionTestCase( + "pi", + doc={"value": math.pi}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(1.1447298858494002), + msg="$ln should return the natural log of pi", + ), +] + +# Property [Value Below One]: $ln returns a negative natural logarithm for inputs between zero and +# one. +LN_BELOW_ONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "half", + doc={"value": 0.5}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(-0.6931471805599453), + msg="$ln should return the natural log of one half", + ), + ExpressionTestCase( + "tenth", + doc={"value": 0.1}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(-2.302585092994046), + msg="$ln should return the natural log of one tenth", + ), + ExpressionTestCase( + "hundredth", + doc={"value": 0.01}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(-4.605170185988091), + msg="$ln should return the natural log of one hundredth", + ), + ExpressionTestCase( + "thousandth", + doc={"value": 0.001}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(-6.907755278982137), + msg="$ln should return the natural log of one thousandth", + ), + ExpressionTestCase( + "ten_thousandth", + doc={"value": 0.0001}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(-9.210340371976182), + msg="$ln should return the natural log of one ten-thousandth", + ), + ExpressionTestCase( + "billionth", + doc={"value": 0.000000001}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(-20.72326583694641), + msg="$ln should return the natural log of one billionth", + ), + ExpressionTestCase( + "five_billionth", + doc={"value": 0.000000005}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(-19.11382792451231), + msg="$ln should return the natural log of five billionths", + ), +] + +# Property [Decimal Precision]: $ln of a decimal128 input returns a full-precision decimal128. +LN_DECIMAL_PRECISION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_e", + doc={"value": Decimal128("2.718281828459045")}, + expression={"$ln": ["$value"]}, + expected=Decimal128("0.9999999999999999134157889710887611"), + msg="$ln should return a full-precision decimal128 near one for a decimal128 near e", + ), + ExpressionTestCase( + "decimal_ten", + doc={"value": Decimal128("10")}, + expression={"$ln": ["$value"]}, + expected=Decimal128("2.302585092994045684017991454684364"), + msg="$ln should return a full-precision decimal128 for decimal128 ten", + ), +] + +LN_MAGNITUDE_ALL_TESTS = ( + LN_ONE_TESTS + LN_ABOVE_ONE_TESTS + LN_BELOW_ONE_TESTS + LN_DECIMAL_PRECISION_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(LN_MAGNITUDE_ALL_TESTS)) +def test_ln_magnitude(collection, test_case: ExpressionTestCase): + """Test $ln natural-logarithm 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/ln/test_ln_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_non_finite.py new file mode 100644 index 000000000..10173d6b8 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_non_finite.py @@ -0,0 +1,68 @@ +"""Tests for $ln of positive infinity and NaN 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 +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + FLOAT_INFINITY, + FLOAT_NAN, +) + +# Property [Infinity]: $ln of positive infinity is infinity of the same type. +LN_INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "float_infinity", + doc={"value": FLOAT_INFINITY}, + expression={"$ln": ["$value"]}, + expected=FLOAT_INFINITY, + msg="$ln should return infinity for float infinity", + ), + ExpressionTestCase( + "decimal128_infinity", + doc={"value": DECIMAL128_INFINITY}, + expression={"$ln": ["$value"]}, + expected=DECIMAL128_INFINITY, + msg="$ln should return decimal128 infinity for decimal128 infinity", + ), +] + +# Property [NaN]: $ln of NaN returns a double NaN, including for a decimal128 NaN input. +LN_NAN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "float_nan", + doc={"value": FLOAT_NAN}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$ln should return NaN for float NaN", + ), + ExpressionTestCase( + "decimal128_nan", + doc={"value": DECIMAL128_NAN}, + expression={"$ln": ["$value"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$ln should return NaN for decimal128 NaN", + ), +] + +LN_NON_FINITE_TESTS = LN_INFINITY_TESTS + LN_NAN_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(LN_NON_FINITE_TESTS)) +def test_ln_non_finite(collection, test_case: ExpressionTestCase): + """Test $ln positive infinity and NaN 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/ln/test_ln_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_null.py new file mode 100644 index 000000000..d7c0ee9e7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_null.py @@ -0,0 +1,43 @@ +"""Tests for $ln 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]: $ln of null or a missing field returns null. +LN_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_value", + doc={"value": None}, + expression={"$ln": ["$value"]}, + expected=None, + msg="$ln should return null for null input", + ), + ExpressionTestCase( + "missing_field", + doc={}, + expression={"$ln": [MISSING]}, + expected=None, + msg="$ln should return null for a missing field", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(LN_NULL_TESTS)) +def test_ln_null(collection, test_case: ExpressionTestCase): + """Test $ln 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/ln/test_ln_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_return_type.py new file mode 100644 index 000000000..22897cd19 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/ln/test_ln_return_type.py @@ -0,0 +1,58 @@ +"""Tests for $ln return type across numeric input 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]: $ln returns double for int32, int64, and double inputs, and decimal for +# decimal128 inputs. +LN_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type_int32", + doc={"value": 1}, + expression={"$type": {"$ln": "$value"}}, + expected="double", + msg="$ln should return a double for an int32 input", + ), + ExpressionTestCase( + "return_type_int64", + doc={"value": Int64(1)}, + expression={"$type": {"$ln": "$value"}}, + expected="double", + msg="$ln should return a double for an int64 input", + ), + ExpressionTestCase( + "return_type_double", + doc={"value": 1.0}, + expression={"$type": {"$ln": "$value"}}, + expected="double", + msg="$ln should return a double for a double input", + ), + ExpressionTestCase( + "return_type_decimal", + doc={"value": Decimal128("1")}, + expression={"$type": {"$ln": "$value"}}, + expected="decimal", + msg="$ln should return a decimal for a decimal128 input", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(LN_RETURN_TYPE_TESTS)) +def test_ln_return_type(collection, test_case: ExpressionTestCase): + """Test $ln return type 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/log/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_base.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_base.py new file mode 100644 index 000000000..c08922b02 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_base.py @@ -0,0 +1,119 @@ +"""Tests for $log with non-integer bases, including fractional bases and bases near one.""" + +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 [Fractional Base]: $log with a positive non-integer base returns the correct result, +# negative when the base is below one. +LOG_FRACTIONAL_BASE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "base_half", + doc={"value": 100, "base": 0.5}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-6.643856189774725), + msg="$log should return the base one half log of one hundred", + ), + ExpressionTestCase( + "base_quarter", + doc={"value": 100, "base": 0.25}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-3.3219280948873626), + msg="$log should return the base one quarter log of one hundred", + ), + ExpressionTestCase( + "base_1_5", + doc={"value": 100, "base": 1.5}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(11.357747174535147), + msg="$log should return the base one and a half log of one hundred", + ), + ExpressionTestCase( + "base_0_9", + doc={"value": 100, "base": 0.9}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-43.708690653565675), + msg="$log should return the base nine tenths log of one hundred", + ), + ExpressionTestCase( + "base_0_1", + doc={"value": 100, "base": 0.1}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-2.0), + msg="$log should return negative two for one hundred in base one tenth", + ), + ExpressionTestCase( + "base_0_01", + doc={"value": 100, "base": 0.01}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-1.0), + msg="$log should return negative one for one hundred in base one hundredth", + ), +] + +# Property [Base Near One]: $log with a base just above or below one returns a large-magnitude +# result of the corresponding sign. +LOG_BASE_NEAR_ONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "base_near_one_above", + doc={"value": 10, "base": 1.0001}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(23027.002203302243), + msg="$log should return a large positive result for a base just above one", + ), + ExpressionTestCase( + "base_near_one_below", + doc={"value": 10, "base": 0.9999}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-23024.699618207327), + msg="$log should return a large negative result for a base just below one", + ), +] + +# Property [Both Below One]: $log with both value and base below one returns a positive result, +# since value and base lie on the same side of one. +LOG_BOTH_BELOW_ONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "quarter_base_half", + doc={"value": 0.25, "base": 0.5}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(2.0), + msg="$log should return two for one quarter in base one half", + ), + ExpressionTestCase( + "thousandth_base_tenth", + doc={"value": 0.001, "base": 0.1}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(3.0), + msg="$log should return three for one thousandth in base one tenth", + ), + ExpressionTestCase( + "three_tenths_base_half", + doc={"value": 0.3, "base": 0.5}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(1.7369655941662063), + msg="$log should return a positive non-integer result for a non-power value and base " + "both below one", + ), +] + +LOG_BASE_ALL_TESTS = LOG_FRACTIONAL_BASE_TESTS + LOG_BASE_NEAR_ONE_TESTS + LOG_BOTH_BELOW_ONE_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(LOG_BASE_ALL_TESTS)) +def test_log_base(collection, test_case: ExpressionTestCase): + """Test $log non-integer base 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/log/test_log_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_boundaries.py new file mode 100644 index 000000000..8c759cc73 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_boundaries.py @@ -0,0 +1,150 @@ +"""Tests for $log at integer, double, and decimal128 representable-range boundaries.""" + +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_MIN_POSITIVE, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + INT32_MAX, + INT64_MAX, +) + +# Property [Integer Boundaries]: $log of large integers, including the int32 and int64 maxima, +# returns a finite double. +LOG_INTEGER_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "base10_billion", + doc={"value": Int64(1_000_000_000), "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(9.0), + msg="$log should return nine for one billion in base ten", + ), + ExpressionTestCase( + "base2_2_to_30", + doc={"value": Int64(1_073_741_824), "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(30.0), + msg="$log should return thirty for two to the thirtieth in base two", + ), + ExpressionTestCase( + "base10_max_int32", + doc={"value": INT32_MAX, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(9.331929865381182), + msg="$log should return the base ten log of INT32_MAX", + ), + ExpressionTestCase( + "base10_max_int64", + doc={"value": INT64_MAX, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(18.964889726830812), + msg="$log should return the base ten log of INT64_MAX", + ), + ExpressionTestCase( + "base2_max_int32", + doc={"value": INT32_MAX, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(30.999999999328196), + msg="$log should return the base two log of INT32_MAX", + ), + ExpressionTestCase( + "base2_max_int64", + doc={"value": INT64_MAX, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(63.0), + msg="$log should return the base two log of INT64_MAX", + ), +] + +# Property [Double Boundaries]: $log at the double subnormal and near-limit range, in the value and +# base arguments, returns the expected large or small-magnitude result. +LOG_DOUBLE_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "value_min_subnormal", + doc={"value": DOUBLE_MIN_SUBNORMAL, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-323.30621534311575), + msg="$log should return a large negative result for the minimum subnormal double value", + ), + ExpressionTestCase( + "value_near_min", + doc={"value": DOUBLE_NEAR_MIN, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-308.0), + msg="$log should return negative three hundred eight for a near-zero positive double value", + ), + ExpressionTestCase( + "value_near_max", + doc={"value": DOUBLE_NEAR_MAX, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(308.0), + msg="$log should return three hundred eight for a near-maximum double value", + ), + ExpressionTestCase( + "value_very_small_positive", + doc={"value": 1e-300, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-299.99999999999994), + msg="$log should return a large negative result for a very small positive double value", + ), + ExpressionTestCase( + "base_near_max", + doc={"value": 10, "base": DOUBLE_NEAR_MAX}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(0.003246753246753247), + msg="$log should return a small positive result for a near-maximum double base", + ), +] + +# Property [Decimal Precision]: $log of decimal128 operands returns a full-precision decimal128. +LOG_DECIMAL_PRECISION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_precision_base10", + doc={"value": Decimal128("1000"), "base": Decimal128("10")}, + expression={"$log": ["$value", "$base"]}, + expected=Decimal128("3.000000000000000000000000000000000"), + msg="$log should return a full-precision decimal128 three for one thousand in base ten", + ), + ExpressionTestCase( + "decimal_precision_base2", + doc={"value": Decimal128("64"), "base": Decimal128("2")}, + expression={"$log": ["$value", "$base"]}, + expected=Decimal128("5.999999999999999999999999999999999"), + msg="$log should return a full-precision decimal128 near six for sixty-four in base two", + ), + ExpressionTestCase( + "decimal_min_positive_value", + doc={"value": DECIMAL128_MIN_POSITIVE, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=Decimal128("-6175.999999999999999999999999999999"), + msg="$log should return a full-precision decimal128 near the exponent for the smallest " + "positive decimal128 value in base ten", + ), +] + +LOG_BOUNDARY_ALL_TESTS = ( + LOG_INTEGER_BOUNDARY_TESTS + LOG_DOUBLE_BOUNDARY_TESTS + LOG_DECIMAL_PRECISION_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(LOG_BOUNDARY_ALL_TESTS)) +def test_log_boundaries(collection, test_case: ExpressionTestCase): + """Test $log representable-range boundary 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/log/test_log_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_errors.py new file mode 100644 index 000000000..8b685248c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_errors.py @@ -0,0 +1,430 @@ +"""Tests for $log domain, type, and arity errors across the value and base arguments.""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, 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 ( + EXPRESSION_TYPE_MISMATCH_ERROR, + LOG_INVALID_BASE_ERROR, + LOG_NON_NUMERIC_BASE_ERROR, + LOG_NON_NUMERIC_VALUE_ERROR, + LOG_NON_POSITIVE_VALUE_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_MAX_NEGATIVE, + DECIMAL128_MIN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ZERO, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_ZERO, + FLOAT_NEGATIVE_INFINITY, + INT32_MIN, + INT64_MIN, +) + +# Property [Value Domain]: $log rejects a non-positive value, including zero, negative zero, +# negative values, and negative infinity, across numeric types. +LOG_VALUE_DOMAIN_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "zero_value", + doc={"value": 0, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject an int32 zero value", + ), + ExpressionTestCase( + "zero_double_value", + doc={"value": DOUBLE_ZERO, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject a double zero value", + ), + ExpressionTestCase( + "negative_zero_double_value", + doc={"value": DOUBLE_NEGATIVE_ZERO, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject a negative zero double value", + ), + ExpressionTestCase( + "zero_decimal_value", + doc={"value": DECIMAL128_ZERO, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject a decimal128 zero value", + ), + ExpressionTestCase( + "negative_zero_decimal_value", + doc={"value": DECIMAL128_NEGATIVE_ZERO, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject a negative zero decimal128 value", + ), + ExpressionTestCase( + "negative_value", + doc={"value": -10, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject a negative int32 value", + ), + ExpressionTestCase( + "negative_fractional_value", + doc={"value": -0.5, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject a negative fractional value", + ), + ExpressionTestCase( + "negative_decimal_value", + doc={"value": Decimal128("-5"), "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject a negative decimal128 value", + ), + ExpressionTestCase( + "int32_min_value", + doc={"value": INT32_MIN, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject an INT32_MIN value", + ), + ExpressionTestCase( + "int64_min_value", + doc={"value": INT64_MIN, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject an INT64_MIN value", + ), + ExpressionTestCase( + "decimal_min_value", + doc={"value": DECIMAL128_MIN, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject the minimum decimal128 value", + ), + ExpressionTestCase( + "decimal_max_negative_value", + doc={"value": DECIMAL128_MAX_NEGATIVE, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject the smallest-magnitude negative decimal128 value", + ), + ExpressionTestCase( + "negative_infinity_value", + doc={"value": FLOAT_NEGATIVE_INFINITY, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject a float negative infinity value", + ), + ExpressionTestCase( + "decimal_negative_infinity_value", + doc={"value": DECIMAL128_NEGATIVE_INFINITY, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_POSITIVE_VALUE_ERROR, + msg="$log should reject a decimal128 negative infinity value", + ), +] + +# Property [Base Domain]: $log rejects a base that is not a positive number other than one, +# including zero, negative bases, negative infinity, and one. +LOG_BASE_DOMAIN_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "zero_base", + doc={"value": 100, "base": 0}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject an int32 zero base", + ), + ExpressionTestCase( + "zero_double_base", + doc={"value": 100, "base": DOUBLE_ZERO}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject a double zero base", + ), + ExpressionTestCase( + "negative_zero_double_base", + doc={"value": 100, "base": DOUBLE_NEGATIVE_ZERO}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject a negative zero double base", + ), + ExpressionTestCase( + "zero_decimal_base", + doc={"value": 100, "base": DECIMAL128_ZERO}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject a decimal128 zero base", + ), + ExpressionTestCase( + "negative_zero_decimal_base", + doc={"value": 100, "base": DECIMAL128_NEGATIVE_ZERO}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject a negative zero decimal128 base", + ), + ExpressionTestCase( + "negative_base", + doc={"value": 100, "base": -10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject a negative int32 base", + ), + ExpressionTestCase( + "negative_base_two", + doc={"value": 100, "base": -2}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject a negative int32 base of minus two", + ), + ExpressionTestCase( + "negative_decimal_base", + doc={"value": 100, "base": Decimal128("-2")}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject a negative decimal128 base", + ), + ExpressionTestCase( + "int32_min_base", + doc={"value": 100, "base": INT32_MIN}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject an INT32_MIN base", + ), + ExpressionTestCase( + "int64_min_base", + doc={"value": 100, "base": INT64_MIN}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject an INT64_MIN base", + ), + ExpressionTestCase( + "decimal_min_base", + doc={"value": 100, "base": DECIMAL128_MIN}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject the minimum decimal128 base", + ), + ExpressionTestCase( + "negative_infinity_base", + doc={"value": 100, "base": FLOAT_NEGATIVE_INFINITY}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject a float negative infinity base", + ), + ExpressionTestCase( + "decimal_negative_infinity_base", + doc={"value": 100, "base": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject a decimal128 negative infinity base", + ), + ExpressionTestCase( + "base_one", + doc={"value": 100, "base": 1}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject an int32 base of one", + ), + ExpressionTestCase( + "base_one_double", + doc={"value": 100, "base": 1.0}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject a double base of one", + ), + ExpressionTestCase( + "base_one_decimal", + doc={"value": 100, "base": Decimal128("1")}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_INVALID_BASE_ERROR, + msg="$log should reject a decimal128 base of one", + ), +] + +# Property [Value Type Strictness]: $log rejects a non-numeric value. +LOG_VALUE_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + *[ + ExpressionTestCase( + f"value_type_{tid}", + doc={"value": val, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_NUMERIC_VALUE_ERROR, + msg=f"$log should reject a {tid} value", + ) + for tid, val in [ + ("string", "abc"), + ("bool", True), + ("array", [1, 2]), + ("object", {"a": 1}), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("regex", Regex("abc")), + ("binary", Binary(b"data")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("timestamp", Timestamp(1, 1)), + ("code", Code("function(){}")), + ] + ], + ExpressionTestCase( + "value_empty_array", + doc={"value": [], "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_NUMERIC_VALUE_ERROR, + msg="$log should reject an empty array value", + ), + ExpressionTestCase( + "value_empty_object", + doc={"value": {}, "base": 10}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_NUMERIC_VALUE_ERROR, + msg="$log should reject an empty object value", + ), +] + +# Property [Base Type Strictness]: $log rejects a non-numeric base. +LOG_BASE_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + *[ + ExpressionTestCase( + f"base_type_{tid}", + doc={"value": 100, "base": val}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_NUMERIC_BASE_ERROR, + msg=f"$log should reject a {tid} base", + ) + for tid, val in [ + ("string", "abc"), + ("bool", True), + ("array", [1, 2]), + ("object", {"a": 1}), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("regex", Regex("abc")), + ("binary", Binary(b"data")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("timestamp", Timestamp(1, 1)), + ("code", Code("function(){}")), + ] + ], + ExpressionTestCase( + "base_empty_array", + doc={"value": 100, "base": []}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_NUMERIC_BASE_ERROR, + msg="$log should reject an empty array base", + ), + ExpressionTestCase( + "base_empty_object", + doc={"value": 100, "base": {}}, + expression={"$log": ["$value", "$base"]}, + error_code=LOG_NON_NUMERIC_BASE_ERROR, + msg="$log should reject an empty object base", + ), +] + +# Property [Non-Numeric Expression and Path Inputs]: array and object expression inputs, and field +# paths that resolve to an array, are delivered to $log as values and rejected as non-numeric in +# the value and base arguments. +LOG_EXPRESSION_INPUT_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "array_value_expression", + doc={"value": 100}, + expression={"$log": [["$value"], 10]}, + error_code=LOG_NON_NUMERIC_VALUE_ERROR, + msg="$log should reject an array expression value without flattening it into arguments", + ), + ExpressionTestCase( + "object_value_expression", + doc={"value": 100}, + expression={"$log": [{"z": "$value"}, 10]}, + error_code=LOG_NON_NUMERIC_VALUE_ERROR, + msg="$log should reject an object expression value rather than evaluating it", + ), + ExpressionTestCase( + "array_path_value", + doc={"a": [{"b": 10}, {"b": 100}]}, + expression={"$log": ["$a.b", 10]}, + error_code=LOG_NON_NUMERIC_VALUE_ERROR, + msg="$log should reject a field-path value that resolves to an array", + ), + ExpressionTestCase( + "array_base_expression", + doc={"base": 10}, + expression={"$log": [100, ["$base"]]}, + error_code=LOG_NON_NUMERIC_BASE_ERROR, + msg="$log should reject an array expression base without flattening it into arguments", + ), + ExpressionTestCase( + "object_base_expression", + doc={"base": 10}, + expression={"$log": [100, {"z": "$base"}]}, + error_code=LOG_NON_NUMERIC_BASE_ERROR, + msg="$log should reject an object expression base rather than evaluating it", + ), + ExpressionTestCase( + "array_path_base", + doc={"a": [{"b": 10}, {"b": 100}]}, + expression={"$log": [100, "$a.b"]}, + error_code=LOG_NON_NUMERIC_BASE_ERROR, + msg="$log should reject a field-path base that resolves to an array", + ), +] + +# Property [Arity]: $log requires exactly two arguments. +LOG_ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "arity_zero", + doc={}, + expression={"$log": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$log should reject zero arguments", + ), + ExpressionTestCase( + "arity_one", + doc={}, + expression={"$log": [100]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$log should reject a single argument", + ), + ExpressionTestCase( + "arity_three", + doc={}, + expression={"$log": [100, 10, 2]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$log should reject three arguments", + ), +] + +LOG_ERROR_ALL_TESTS = ( + LOG_VALUE_DOMAIN_ERROR_TESTS + + LOG_BASE_DOMAIN_ERROR_TESTS + + LOG_VALUE_TYPE_ERROR_TESTS + + LOG_BASE_TYPE_ERROR_TESTS + + LOG_EXPRESSION_INPUT_ERROR_TESTS + + LOG_ARITY_ERROR_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(LOG_ERROR_ALL_TESTS)) +def test_log_errors(collection, test_case: ExpressionTestCase): + """Test $log domain, type, and arity 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/log/test_log_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_input_forms.py new file mode 100644 index 000000000..bdff51615 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_input_forms.py @@ -0,0 +1,70 @@ +"""Tests for $log argument form, literal input, and nested expression input.""" + +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 + +# Property [Argument Form]: $log requires its two arguments in an array and rejects a bare +# non-array argument. +LOG_ARGUMENT_FORM_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "bare_non_array", + doc={"value": 100}, + expression={"$log": "$value"}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$log should reject a bare non-array argument, requiring its value and base in an " + "array", + ), +] + +# Property [Literal Input]: $log evaluates inline literal value and base arguments. +LOG_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_input", + doc={}, + expression={"$log": [100, 10]}, + expected=pytest.approx(2.0), + msg="$log should return two for inline literal one hundred in base ten", + ), +] + +# Property [Expression Input]: $log evaluates a nested expression in the value or base argument +# before taking the logarithm. +LOG_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "expression_value", + doc={}, + expression={"$log": [{"$add": [50, 50]}, 10]}, + expected=pytest.approx(2.0), + msg="$log should evaluate a nested expression value argument", + ), + ExpressionTestCase( + "expression_base", + doc={}, + expression={"$log": [100, {"$add": [5, 5]}]}, + expected=pytest.approx(2.0), + msg="$log should evaluate a nested expression base argument", + ), +] + +LOG_INPUT_FORM_TESTS = LOG_ARGUMENT_FORM_TESTS + LOG_LITERAL_TESTS + LOG_EXPRESSION_INPUT_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(LOG_INPUT_FORM_TESTS)) +def test_log_input_forms(collection, test_case: ExpressionTestCase): + """Test $log 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/log/test_log_magnitude.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_magnitude.py new file mode 100644 index 000000000..eb1f82e77 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_magnitude.py @@ -0,0 +1,417 @@ +"""Tests for $log core logarithm values across base, sign, and numeric type.""" + +import math + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import DOUBLE_ZERO + +# Property [Identity]: $log of one is zero for any base and numeric type. +LOG_IDENTITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "one_base10", + doc={"value": 1, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=DOUBLE_ZERO, + msg="$log should return zero for one in base ten", + ), + ExpressionTestCase( + "one_base2", + doc={"value": 1, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=DOUBLE_ZERO, + msg="$log should return zero for one in base two", + ), + ExpressionTestCase( + "one_base5", + doc={"value": 1, "base": 5}, + expression={"$log": ["$value", "$base"]}, + expected=DOUBLE_ZERO, + msg="$log should return zero for one in base five", + ), +] + +# Property [Base Equals Value]: $log of a value in its own base is one. +LOG_BASE_EQUALS_VALUE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "same_ten", + doc={"value": 10, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(1.0), + msg="$log should return one when value equals base ten", + ), + ExpressionTestCase( + "same_two", + doc={"value": 2, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(1.0), + msg="$log should return one when value equals base two", + ), + ExpressionTestCase( + "same_five", + doc={"value": 5, "base": 5}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(1.0), + msg="$log should return one when value equals base five", + ), +] + +# Property [Same Type]: $log of same-typed value and base returns the correct result per type. +LOG_SAME_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "base10_hundred_int32", + doc={"value": 100, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(2.0), + msg="$log should return two for one hundred in base ten int32", + ), + ExpressionTestCase( + "base10_thousand_int64", + doc={"value": Int64(1000), "base": Int64(10)}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(3.0), + msg="$log should return three for one thousand in base ten int64", + ), + ExpressionTestCase( + "base10_ten_double", + doc={"value": 10.0, "base": 10.0}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(1.0), + msg="$log should return one for ten in base ten double", + ), + ExpressionTestCase( + "base10_hundred_decimal", + doc={"value": Decimal128("100"), "base": Decimal128("10")}, + expression={"$log": ["$value", "$base"]}, + expected=Decimal128("2"), + msg="$log should return two for one hundred in base ten decimal128", + ), + ExpressionTestCase( + "base2_eight_int32", + doc={"value": 8, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(3.0), + msg="$log should return three for eight in base two int32", + ), + ExpressionTestCase( + "base2_1024_int64", + doc={"value": Int64(1024), "base": Int64(2)}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(10.0), + msg="$log should return ten for 1024 in base two int64", + ), + ExpressionTestCase( + "base2_sixteen_double", + doc={"value": 16.0, "base": 2.0}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(4.0), + msg="$log should return four for sixteen in base two double", + ), + ExpressionTestCase( + "base2_32_decimal", + doc={"value": Decimal128("32"), "base": Decimal128("2")}, + expression={"$log": ["$value", "$base"]}, + expected=Decimal128("5"), + msg="$log should return five for thirty-two in base two decimal128", + ), +] + +# Property [Mixed Type]: $log of mixed numeric types returns decimal128 when either operand is +# decimal128 and double otherwise. +LOG_MIXED_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_int64", + doc={"value": 100, "base": Int64(10)}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(2.0), + msg="$log should return two for int32 value and int64 base", + ), + ExpressionTestCase( + "int32_double", + doc={"value": 100, "base": 10.0}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(2.0), + msg="$log should return two for int32 value and double base", + ), + ExpressionTestCase( + "int32_decimal", + doc={"value": 100, "base": Decimal128("10")}, + expression={"$log": ["$value", "$base"]}, + expected=Decimal128("2"), + msg="$log should return decimal128 two for int32 value and decimal128 base", + ), + ExpressionTestCase( + "int64_double", + doc={"value": Int64(1000), "base": 10.0}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(3.0), + msg="$log should return three for int64 value and double base", + ), + ExpressionTestCase( + "int64_decimal", + doc={"value": Int64(1000), "base": Decimal128("10")}, + expression={"$log": ["$value", "$base"]}, + expected=Decimal128("3.000000000000000000000000000000000"), + msg="$log should return full-precision decimal128 three for int64 and decimal128 base", + ), + ExpressionTestCase( + "double_decimal", + doc={"value": 100.0, "base": Decimal128("10")}, + expression={"$log": ["$value", "$base"]}, + expected=Decimal128("2"), + msg="$log should return decimal128 two for double value and decimal128 base", + ), +] + +# Property [Powers Of Base]: $log of an exact power of the base returns the integer exponent. +LOG_POWER_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "base10_ten_thousand", + doc={"value": 10_000, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(4.0), + msg="$log should return four for ten thousand in base ten", + ), + ExpressionTestCase( + "base10_million", + doc={"value": 1_000_000, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(6.0), + msg="$log should return six for one million in base ten", + ), + ExpressionTestCase( + "base2_64", + doc={"value": 64, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(6.0), + msg="$log should return six for sixty-four in base two", + ), + ExpressionTestCase( + "base2_128", + doc={"value": 128, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(7.0), + msg="$log should return seven for one hundred twenty-eight in base two", + ), + ExpressionTestCase( + "base2_256", + doc={"value": 256, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(8.0), + msg="$log should return eight for two hundred fifty-six in base two", + ), + ExpressionTestCase( + "base5_125", + doc={"value": 125, "base": 5}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(3.0), + msg="$log should return three for one hundred twenty-five in base five", + ), + ExpressionTestCase( + "base5_625", + doc={"value": 625, "base": 5}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(4.0), + msg="$log should return four for six hundred twenty-five in base five", + ), + ExpressionTestCase( + "base3_27", + doc={"value": 27, "base": 3}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(3.0), + msg="$log should return three for twenty-seven in base three", + ), + ExpressionTestCase( + "base3_81", + doc={"value": 81, "base": 3}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(4.0), + msg="$log should return four for eighty-one in base three", + ), +] + +# Property [Fractional Result]: $log of a non-power value returns the correct non-integer result. +LOG_FRACTIONAL_RESULT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "base10_50", + doc={"value": 50, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(1.6989700043360185), + msg="$log should return the base ten log of fifty", + ), + ExpressionTestCase( + "base10_5", + doc={"value": 5, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(0.6989700043360187), + msg="$log should return the base ten log of five", + ), + ExpressionTestCase( + "base2_3", + doc={"value": 3, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(1.5849625007211563), + msg="$log should return the base two log of three", + ), + ExpressionTestCase( + "base2_10", + doc={"value": 10, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(3.3219280948873626), + msg="$log should return the base two log of ten", + ), + ExpressionTestCase( + "base3_100", + doc={"value": 100, "base": 3}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(4.19180654857877), + msg="$log should return the base three log of one hundred", + ), + ExpressionTestCase( + "base10_e", + doc={"value": math.e, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(0.43429448190325176), + msg="$log should return the base ten log of e", + ), + ExpressionTestCase( + "base10_pi", + doc={"value": math.pi, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(0.4971498726941338), + msg="$log should return the base ten log of pi", + ), + ExpressionTestCase( + "base2_e", + doc={"value": math.e, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(1.4426950408889634), + msg="$log should return the base two log of e", + ), + ExpressionTestCase( + "base2_pi", + doc={"value": math.pi, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(1.651496129472319), + msg="$log should return the base two log of pi", + ), + ExpressionTestCase( + "base_e_e", + doc={"value": math.e, "base": math.e}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(1.0), + msg="$log should return one for e in base e", + ), + ExpressionTestCase( + "base_e_e_squared", + doc={"value": math.e**2, "base": math.e}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(2.0), + msg="$log should return two for e squared in base e", + ), + ExpressionTestCase( + "base2_hundred", + doc={"value": 100, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(6.643856189774725), + msg="$log should return the base two log of one hundred", + ), + ExpressionTestCase( + "base5_hundred", + doc={"value": 100, "base": 5}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(2.8613531161467867), + msg="$log should return the base five log of one hundred", + ), + ExpressionTestCase( + "base2_thousand", + doc={"value": 1000, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(9.965784284662087), + msg="$log should return the base two log of one thousand", + ), +] + +# Property [Fractional Value]: $log of a value between zero and one returns a negative result. +LOG_FRACTIONAL_VALUE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "base10_tenth", + doc={"value": 0.1, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-1.0), + msg="$log should return negative one for one tenth in base ten", + ), + ExpressionTestCase( + "base10_hundredth", + doc={"value": 0.01, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-2.0), + msg="$log should return negative two for one hundredth in base ten", + ), + ExpressionTestCase( + "base2_half", + doc={"value": 0.5, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-1.0), + msg="$log should return negative one for one half in base two", + ), + ExpressionTestCase( + "base2_quarter", + doc={"value": 0.25, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-2.0), + msg="$log should return negative two for one quarter in base two", + ), + ExpressionTestCase( + "base2_eighth", + doc={"value": 0.125, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-3.0), + msg="$log should return negative three for one eighth in base two", + ), + ExpressionTestCase( + "base10_billionth", + doc={"value": 0.000000001, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-9.0), + msg="$log should return negative nine for one billionth in base ten", + ), + ExpressionTestCase( + "base10_five_billionth", + doc={"value": 0.000000005, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(-8.301029995663981), + msg="$log should return the base ten log of five billionths", + ), +] + +LOG_MAGNITUDE_ALL_TESTS = ( + LOG_IDENTITY_TESTS + + LOG_BASE_EQUALS_VALUE_TESTS + + LOG_SAME_TYPE_TESTS + + LOG_MIXED_TYPE_TESTS + + LOG_POWER_TESTS + + LOG_FRACTIONAL_RESULT_TESTS + + LOG_FRACTIONAL_VALUE_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(LOG_MAGNITUDE_ALL_TESTS)) +def test_log_magnitude(collection, test_case: ExpressionTestCase): + """Test $log logarithm 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/log/test_log_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_non_finite.py new file mode 100644 index 000000000..6fc7c8d53 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_non_finite.py @@ -0,0 +1,127 @@ +"""Tests for $log with infinite and NaN value and base 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 +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DOUBLE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, +) + +# Property [Infinity]: $log of infinity is infinity, $log to an infinite base is zero, and infinity +# in both operands is NaN. +LOG_INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "infinity_value", + doc={"value": FLOAT_INFINITY, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=FLOAT_INFINITY, + msg="$log should return infinity for an infinite value in base ten", + ), + ExpressionTestCase( + "infinity_value_base2", + doc={"value": FLOAT_INFINITY, "base": 2}, + expression={"$log": ["$value", "$base"]}, + expected=FLOAT_INFINITY, + msg="$log should return infinity for an infinite value in base two", + ), + ExpressionTestCase( + "decimal_infinity_value", + doc={"value": DECIMAL128_INFINITY, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=DECIMAL128_INFINITY, + msg="$log should return decimal128 infinity for an infinite decimal128 value", + ), + ExpressionTestCase( + "infinity_base", + doc={"value": 10, "base": FLOAT_INFINITY}, + expression={"$log": ["$value", "$base"]}, + expected=DOUBLE_ZERO, + msg="$log should return zero for a finite value in an infinite base", + ), + ExpressionTestCase( + "both_infinity", + doc={"value": FLOAT_INFINITY, "base": FLOAT_INFINITY}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$log should return NaN when both value and base are infinite", + ), + ExpressionTestCase( + "decimal_both_infinity", + doc={"value": DECIMAL128_INFINITY, "base": DECIMAL128_INFINITY}, + expression={"$log": ["$value", "$base"]}, + expected=DECIMAL128_NAN, + msg="$log should return decimal128 NaN when both value and base are infinite decimal128", + ), +] + +# Property [NaN]: $log of a NaN value or base returns a double NaN, including for decimal128 NaN +# inputs. +LOG_NAN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nan_value", + doc={"value": FLOAT_NAN, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$log should return NaN for a NaN value", + ), + ExpressionTestCase( + "nan_base", + doc={"value": 100, "base": FLOAT_NAN}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$log should return NaN for a NaN base", + ), + ExpressionTestCase( + "both_nan", + doc={"value": FLOAT_NAN, "base": FLOAT_NAN}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$log should return NaN when both value and base are NaN", + ), + ExpressionTestCase( + "decimal_nan_value", + doc={"value": DECIMAL128_NAN, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$log should return NaN for a decimal128 NaN value", + ), + ExpressionTestCase( + "decimal_nan_base", + doc={"value": 100, "base": DECIMAL128_NAN}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$log should return NaN for a decimal128 NaN base", + ), + ExpressionTestCase( + "decimal_both_nan", + doc={"value": DECIMAL128_NAN, "base": DECIMAL128_NAN}, + expression={"$log": ["$value", "$base"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$log should return NaN when both value and base are decimal128 NaN", + ), +] + +LOG_NON_FINITE_TESTS = LOG_INFINITY_TESTS + LOG_NAN_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(LOG_NON_FINITE_TESTS)) +def test_log_non_finite(collection, test_case: ExpressionTestCase): + """Test $log infinity and NaN 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/log/test_log_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_null.py new file mode 100644 index 000000000..3d383e791 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_null.py @@ -0,0 +1,86 @@ +"""Tests for $log null and missing propagation across the value and base arguments.""" + +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]: $log returns null when either the value or the base is null or a +# missing field. +LOG_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_value", + doc={"value": None, "base": 10}, + expression={"$log": ["$value", "$base"]}, + expected=None, + msg="$log should return null for a null value", + ), + ExpressionTestCase( + "null_base", + doc={"value": 10, "base": None}, + expression={"$log": ["$value", "$base"]}, + expected=None, + msg="$log should return null for a null base", + ), + ExpressionTestCase( + "both_null", + doc={"value": None, "base": None}, + expression={"$log": ["$value", "$base"]}, + expected=None, + msg="$log should return null when both value and base are null", + ), + ExpressionTestCase( + "missing_value", + doc={"base": 10}, + expression={"$log": [MISSING, "$base"]}, + expected=None, + msg="$log should return null for a missing value field", + ), + ExpressionTestCase( + "missing_base", + doc={"value": 10}, + expression={"$log": ["$value", MISSING]}, + expected=None, + msg="$log should return null for a missing base field", + ), + ExpressionTestCase( + "both_missing", + doc={}, + expression={"$log": [MISSING, MISSING]}, + expected=None, + msg="$log should return null when both value and base fields are missing", + ), + ExpressionTestCase( + "null_value_missing_base", + doc={"value": None}, + expression={"$log": ["$value", MISSING]}, + expected=None, + msg="$log should return null when the value is null and the base is missing", + ), + ExpressionTestCase( + "missing_value_null_base", + doc={"base": None}, + expression={"$log": [MISSING, "$base"]}, + expected=None, + msg="$log should return null when the value is missing and the base is null", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(LOG_NULL_TESTS)) +def test_log_null(collection, test_case: ExpressionTestCase): + """Test $log null and missing 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/log/test_log_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_return_type.py new file mode 100644 index 000000000..6699dac85 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log/test_log_return_type.py @@ -0,0 +1,72 @@ +"""Tests for $log return type across value and base 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]: $log returns double unless either operand is decimal128, in which case it +# returns decimal. +LOG_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type_int32", + doc={"value": 100, "base": 10}, + expression={"$type": {"$log": ["$value", "$base"]}}, + expected="double", + msg="$log should return a double for int32 value and base", + ), + ExpressionTestCase( + "return_type_int64", + doc={"value": Int64(1000), "base": Int64(10)}, + expression={"$type": {"$log": ["$value", "$base"]}}, + expected="double", + msg="$log should return a double for int64 value and base", + ), + ExpressionTestCase( + "return_type_double", + doc={"value": 10.0, "base": 10.0}, + expression={"$type": {"$log": ["$value", "$base"]}}, + expected="double", + msg="$log should return a double for double value and base", + ), + ExpressionTestCase( + "return_type_value_decimal", + doc={"value": Decimal128("100"), "base": 10}, + expression={"$type": {"$log": ["$value", "$base"]}}, + expected="decimal", + msg="$log should return a decimal for a decimal128 value and int32 base", + ), + ExpressionTestCase( + "return_type_base_decimal", + doc={"value": 100, "base": Decimal128("10")}, + expression={"$type": {"$log": ["$value", "$base"]}}, + expected="decimal", + msg="$log should return a decimal for an int32 value and decimal128 base", + ), + ExpressionTestCase( + "return_type_both_decimal", + doc={"value": Decimal128("100"), "base": Decimal128("10")}, + expression={"$type": {"$log": ["$value", "$base"]}}, + expected="decimal", + msg="$log should return a decimal for decimal128 value and base", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(LOG_RETURN_TYPE_TESTS)) +def test_log_return_type(collection, test_case: ExpressionTestCase): + """Test $log return type 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/log10/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_boundaries.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_boundaries.py new file mode 100644 index 000000000..cb3599746 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_boundaries.py @@ -0,0 +1,160 @@ +"""Tests for $log10 at representable-range boundaries, including subnormal and extreme inputs.""" + +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_MAX, + DECIMAL128_MIN_POSITIVE, + DECIMAL128_SMALL_EXPONENT, + DECIMAL128_TRAILING_ZERO, + DECIMAL128_ZERO, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEAR_MAX, + DOUBLE_NEAR_MIN, + DOUBLE_PRECISION_LOSS, + INT32_MAX, + INT32_MAX_MINUS_1, + INT64_MAX, + INT64_MAX_MINUS_1, +) + +# Property [Integer Boundaries]: $log10 of the largest representable integers returns a finite +# double. +LOG10_INTEGER_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_max", + doc={"value": INT32_MAX}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(9.331929865381182), + msg="$log10 should return the base-ten log of INT32_MAX", + ), + ExpressionTestCase( + "int32_max_minus_1", + doc={"value": INT32_MAX_MINUS_1}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(9.33192986517895), + msg="$log10 should return the base-ten log of INT32_MAX minus one", + ), + ExpressionTestCase( + "int64_max", + doc={"value": INT64_MAX}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(18.964889726830815), + msg="$log10 should return the base-ten log of INT64_MAX", + ), + ExpressionTestCase( + "int64_max_minus_1", + doc={"value": INT64_MAX_MINUS_1}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(18.964889726830815), + msg="$log10 should return the base-ten log of INT64_MAX minus one", + ), +] + +# Property [Double Boundaries]: $log10 at the double subnormal and near-limit range returns the +# expected large-magnitude values. +LOG10_DOUBLE_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_min_subnormal", + doc={"value": DOUBLE_MIN_SUBNORMAL}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(-323.3062153431158), + msg="$log10 should return a large negative value for the minimum subnormal double", + ), + ExpressionTestCase( + "double_near_min", + doc={"value": DOUBLE_NEAR_MIN}, + expression={"$log10": ["$value"]}, + expected=-308.0, + msg="$log10 should return negative three hundred eight for a near-zero positive double", + ), + ExpressionTestCase( + "double_near_max", + doc={"value": DOUBLE_NEAR_MAX}, + expression={"$log10": ["$value"]}, + expected=308.0, + msg="$log10 should return three hundred eight for a near-maximum double", + ), + ExpressionTestCase( + "double_max_safe_integer", + doc={"value": DOUBLE_MAX_SAFE_INTEGER}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(15.954589770191003), + msg="$log10 should return the base-ten log of the maximum safe integer double", + ), + ExpressionTestCase( + "double_precision_loss", + doc={"value": DOUBLE_PRECISION_LOSS}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(15.954589770191003), + msg="$log10 should return the same value as the maximum safe integer for the next " + "integer double, which is not representable", + ), + ExpressionTestCase( + "very_small_positive", + doc={"value": 1e-300}, + expression={"$log10": ["$value"]}, + expected=-300.0, + msg="$log10 should return negative three hundred for a very small positive double", + ), +] + +# Property [Decimal128 Boundaries]: $log10 of extreme decimal128 exponents returns a full-precision +# decimal128 result. +LOG10_DECIMAL_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_small_exponent", + doc={"value": DECIMAL128_SMALL_EXPONENT}, + expression={"$log10": ["$value"]}, + expected=Decimal128("-6143"), + msg="$log10 should return the exact negative exponent for a decimal128 with a small " + "exponent", + ), + ExpressionTestCase( + "decimal_min_positive", + doc={"value": DECIMAL128_MIN_POSITIVE}, + expression={"$log10": ["$value"]}, + expected=Decimal128("-6176"), + msg="$log10 should return the exact negative exponent for the smallest positive decimal128", + ), + ExpressionTestCase( + "decimal_max", + doc={"value": DECIMAL128_MAX}, + expression={"$log10": ["$value"]}, + expected=Decimal128("6145"), + msg="$log10 should return the exponent for the maximum decimal128", + ), + ExpressionTestCase( + "decimal_trailing_zero", + doc={"value": DECIMAL128_TRAILING_ZERO}, + expression={"$log10": ["$value"]}, + expected=DECIMAL128_ZERO, + msg="$log10 should return zero for a decimal128 one with a trailing zero", + ), +] + +LOG10_BOUNDARY_ALL_TESTS = ( + LOG10_INTEGER_BOUNDARY_TESTS + LOG10_DOUBLE_BOUNDARY_TESTS + LOG10_DECIMAL_BOUNDARY_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(LOG10_BOUNDARY_ALL_TESTS)) +def test_log10_boundaries(collection, test_case: ExpressionTestCase): + """Test $log10 representable-range boundary 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/log10/test_log10_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_errors.py new file mode 100644 index 000000000..d31d80776 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_errors.py @@ -0,0 +1,254 @@ +"""Tests for $log10 domain, type, and arity errors.""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, 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 ( + EXPRESSION_TYPE_MISMATCH_ERROR, + LOG10_NON_POSITIVE_INPUT_ERROR, + NON_NUMERIC_TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_MAX_NEGATIVE, + DECIMAL128_MIN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ZERO, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_ZERO, + FLOAT_NEGATIVE_INFINITY, + INT32_MIN, + INT64_MIN, +) + +# Property [Domain]: $log10 rejects non-positive inputs, including zero, negative zero, negative +# values, and negative infinity. +LOG10_DOMAIN_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "zero_int32", + doc={"value": 0}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject int32 zero", + ), + ExpressionTestCase( + "zero_double", + doc={"value": DOUBLE_ZERO}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject double zero", + ), + ExpressionTestCase( + "zero_decimal", + doc={"value": DECIMAL128_ZERO}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject decimal128 zero", + ), + ExpressionTestCase( + "negative_zero_double", + doc={"value": DOUBLE_NEGATIVE_ZERO}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject negative zero double", + ), + ExpressionTestCase( + "negative_zero_decimal", + doc={"value": DECIMAL128_NEGATIVE_ZERO}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject negative zero decimal128", + ), + ExpressionTestCase( + "negative_int32", + doc={"value": -1}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject a negative integer", + ), + ExpressionTestCase( + "negative_ten", + doc={"value": -10}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject negative ten", + ), + ExpressionTestCase( + "negative_double", + doc={"value": -0.5}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject a negative double", + ), + ExpressionTestCase( + "negative_decimal", + doc={"value": Decimal128("-1")}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject a negative decimal128", + ), + ExpressionTestCase( + "int32_min", + doc={"value": INT32_MIN}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject INT32_MIN", + ), + ExpressionTestCase( + "int64_min", + doc={"value": INT64_MIN}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject INT64_MIN", + ), + ExpressionTestCase( + "decimal_min", + doc={"value": DECIMAL128_MIN}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject the minimum decimal128", + ), + ExpressionTestCase( + "decimal_max_negative", + doc={"value": DECIMAL128_MAX_NEGATIVE}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject the smallest-magnitude negative decimal128", + ), + ExpressionTestCase( + "float_negative_infinity", + doc={"value": FLOAT_NEGATIVE_INFINITY}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject float negative infinity", + ), + ExpressionTestCase( + "decimal128_negative_infinity", + doc={"value": DECIMAL128_NEGATIVE_INFINITY}, + expression={"$log10": ["$value"]}, + error_code=LOG10_NON_POSITIVE_INPUT_ERROR, + msg="$log10 should reject decimal128 negative infinity", + ), +] + +# Property [Type Strictness]: $log10 rejects non-numeric input types. +LOG10_TYPE_ERROR_TESTS: list[ExpressionTestCase] = [ + *[ + ExpressionTestCase( + f"type_{tid}", + doc={"value": val}, + expression={"$log10": ["$value"]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg=f"$log10 should reject a {tid} input", + ) + for tid, val in [ + ("string", "abc"), + ("bool", True), + ("array", [1, 2]), + ("object", {"a": 1}), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("objectid", ObjectId("507f1f77bcf86cd799439011")), + ("regex", Regex("abc")), + ("binary", Binary(b"data")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ("timestamp", Timestamp(1, 1)), + ("code", Code("function(){}")), + ] + ], + ExpressionTestCase( + "empty_array", + doc={"value": []}, + expression={"$log10": ["$value"]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$log10 should reject an empty array input", + ), + ExpressionTestCase( + "empty_object", + doc={"value": {}}, + expression={"$log10": ["$value"]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$log10 should reject an empty object input", + ), +] + +# Property [Non-Numeric Expression and Path Inputs]: array and object expression inputs, and field +# paths that resolve to an array, are delivered to $log10 as values and rejected as non-numeric. +LOG10_EXPRESSION_INPUT_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "array_expression_input", + doc={"value": 10}, + expression={"$log10": [["$value"]]}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$log10 should reject an array expression input without flattening it into arguments", + ), + ExpressionTestCase( + "object_expression_input", + doc={"value": 10}, + expression={"$log10": {"z": "$value"}}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$log10 should reject an object expression input as a value rather than evaluating it", + ), + ExpressionTestCase( + "array_of_objects_path", + doc={"a": [{"b": 10}, {"b": 100}]}, + expression={"$log10": "$a.b"}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$log10 should reject a field path that resolves to an array from an array of objects", + ), + ExpressionTestCase( + "array_index_path", + doc={"a": [{"b": 10}, {"b": 100}]}, + expression={"$log10": "$a.0.b"}, + error_code=NON_NUMERIC_TYPE_MISMATCH_ERROR, + msg="$log10 should reject an array-index field path that resolves to an array in an " + "aggregation expression", + ), +] + +# Property [Arity]: $log10 requires exactly one argument. +LOG10_ARITY_ERROR_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "arity_zero", + doc={}, + expression={"$log10": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$log10 should reject zero arguments", + ), + ExpressionTestCase( + "arity_two", + doc={}, + expression={"$log10": [1, 2]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$log10 should reject two arguments", + ), +] + +LOG10_ERROR_ALL_TESTS = ( + LOG10_DOMAIN_ERROR_TESTS + + LOG10_TYPE_ERROR_TESTS + + LOG10_EXPRESSION_INPUT_ERROR_TESTS + + LOG10_ARITY_ERROR_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(LOG10_ERROR_ALL_TESTS)) +def test_log10_errors(collection, test_case: ExpressionTestCase): + """Test $log10 domain, type, and arity 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/log10/test_log10_input_forms.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_input_forms.py new file mode 100644 index 000000000..62da722c4 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_input_forms.py @@ -0,0 +1,70 @@ +"""Tests for $log10 argument forms, literal input, and nested expression input.""" + +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 DOUBLE_ZERO + +# Property [Argument Form]: $log10 accepts its single argument bare or wrapped in a one-element +# array. +LOG10_ARGUMENT_FORM_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "form_array", + doc={"value": 1}, + expression={"$log10": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$log10 should accept its argument wrapped in a one-element array", + ), + ExpressionTestCase( + "form_bare", + doc={"value": 1}, + expression={"$log10": "$value"}, + expected=DOUBLE_ZERO, + msg="$log10 should accept its argument without an array wrapper", + ), +] + +# Property [Literal Input]: $log10 evaluates an inline literal argument, not only document fields. +LOG10_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "literal_input", + doc={}, + expression={"$log10": [10]}, + expected=1.0, + msg="$log10 should return one for an inline literal ten", + ), +] + +# Property [Expression Input]: $log10 evaluates a nested expression argument before taking the log. +LOG10_EXPRESSION_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nested_multiply", + doc={}, + expression={"$log10": {"$multiply": [10, 10]}}, + expected=2.0, + msg="$log10 should evaluate a nested $multiply expression argument", + ), +] + +LOG10_INPUT_FORM_TESTS = ( + LOG10_ARGUMENT_FORM_TESTS + LOG10_LITERAL_TESTS + LOG10_EXPRESSION_INPUT_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(LOG10_INPUT_FORM_TESTS)) +def test_log10_input_forms(collection, test_case: ExpressionTestCase): + """Test $log10 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/log10/test_log10_magnitude.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_magnitude.py new file mode 100644 index 000000000..d1a25bbb9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_magnitude.py @@ -0,0 +1,332 @@ +"""Tests for $log10 core base-ten logarithm values across sign and numeric type.""" + +import math + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import DECIMAL128_ZERO, DOUBLE_ZERO + +# Property [Identity]: $log10 of one is zero for every numeric type. +LOG10_ONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "one_int32", + doc={"value": 1}, + expression={"$log10": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$log10 should return zero for int32 one", + ), + ExpressionTestCase( + "one_int64", + doc={"value": Int64(1)}, + expression={"$log10": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$log10 should return zero for int64 one", + ), + ExpressionTestCase( + "one_double", + doc={"value": 1.0}, + expression={"$log10": ["$value"]}, + expected=DOUBLE_ZERO, + msg="$log10 should return zero for double one", + ), + ExpressionTestCase( + "one_decimal", + doc={"value": Decimal128("1")}, + expression={"$log10": ["$value"]}, + expected=DECIMAL128_ZERO, + msg="$log10 should return zero for decimal128 one", + ), +] + +# Property [Positive Powers of Ten]: $log10 of a positive power of ten returns its exact integer +# exponent as a double. +LOG10_POSITIVE_POWER_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "ten_int32", + doc={"value": 10}, + expression={"$log10": ["$value"]}, + expected=1.0, + msg="$log10 should return one for int32 ten", + ), + ExpressionTestCase( + "ten_int64", + doc={"value": Int64(10)}, + expression={"$log10": ["$value"]}, + expected=1.0, + msg="$log10 should return one for int64 ten", + ), + ExpressionTestCase( + "ten_double", + doc={"value": 10.0}, + expression={"$log10": ["$value"]}, + expected=1.0, + msg="$log10 should return one for double ten", + ), + ExpressionTestCase( + "hundred", + doc={"value": 100}, + expression={"$log10": ["$value"]}, + expected=2.0, + msg="$log10 should return two for one hundred", + ), + ExpressionTestCase( + "thousand", + doc={"value": 1000}, + expression={"$log10": ["$value"]}, + expected=3.0, + msg="$log10 should return three for one thousand", + ), + ExpressionTestCase( + "ten_thousand", + doc={"value": 10_000}, + expression={"$log10": ["$value"]}, + expected=4.0, + msg="$log10 should return four for ten thousand", + ), + ExpressionTestCase( + "million", + doc={"value": 1_000_000}, + expression={"$log10": ["$value"]}, + expected=6.0, + msg="$log10 should return six for one million", + ), + ExpressionTestCase( + "billion", + doc={"value": Int64(1_000_000_000)}, + expression={"$log10": ["$value"]}, + expected=9.0, + msg="$log10 should return nine for one billion int64", + ), + ExpressionTestCase( + "ten_billion", + doc={"value": Int64(10_000_000_000)}, + expression={"$log10": ["$value"]}, + expected=10.0, + msg="$log10 should return ten for ten billion int64", + ), + ExpressionTestCase( + "quadrillion", + doc={"value": 1e15}, + expression={"$log10": ["$value"]}, + expected=15.0, + msg="$log10 should return fifteen for one quadrillion", + ), + ExpressionTestCase( + "hundred_quintillion", + doc={"value": 1e20}, + expression={"$log10": ["$value"]}, + expected=20.0, + msg="$log10 should return twenty for one hundred quintillion", + ), +] + +# Property [Negative Powers of Ten]: $log10 of a fractional power of ten returns its exact negative +# integer exponent as a double. +LOG10_NEGATIVE_POWER_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "tenth", + doc={"value": 0.1}, + expression={"$log10": ["$value"]}, + expected=-1.0, + msg="$log10 should return negative one for one tenth", + ), + ExpressionTestCase( + "hundredth", + doc={"value": 0.01}, + expression={"$log10": ["$value"]}, + expected=-2.0, + msg="$log10 should return negative two for one hundredth", + ), + ExpressionTestCase( + "thousandth", + doc={"value": 0.001}, + expression={"$log10": ["$value"]}, + expected=-3.0, + msg="$log10 should return negative three for one thousandth", + ), + ExpressionTestCase( + "ten_thousandth", + doc={"value": 0.0001}, + expression={"$log10": ["$value"]}, + expected=-4.0, + msg="$log10 should return negative four for one ten-thousandth", + ), + ExpressionTestCase( + "hundred_thousandth", + doc={"value": 1e-5}, + expression={"$log10": ["$value"]}, + expected=-5.0, + msg="$log10 should return negative five for one hundred-thousandth", + ), + ExpressionTestCase( + "billionth", + doc={"value": 1e-9}, + expression={"$log10": ["$value"]}, + expected=-9.0, + msg="$log10 should return negative nine for one billionth", + ), + ExpressionTestCase( + "ten_billionth", + doc={"value": 1e-10}, + expression={"$log10": ["$value"]}, + expected=-10.0, + msg="$log10 should return negative ten for one ten-billionth", + ), + ExpressionTestCase( + "quadrillionth", + doc={"value": 1e-15}, + expression={"$log10": ["$value"]}, + expected=-15.0, + msg="$log10 should return negative fifteen for one quadrillionth", + ), +] + +# Property [Non-Power Values]: $log10 of a value that is not a power of ten returns its +# base-ten logarithm. +LOG10_NON_POWER_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "two", + doc={"value": 2}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(0.3010299956639812), + msg="$log10 should return the base-ten log of two", + ), + ExpressionTestCase( + "five", + doc={"value": 5}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(0.6989700043360189), + msg="$log10 should return the base-ten log of five", + ), + ExpressionTestCase( + "seven", + doc={"value": 7}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(0.8450980400142568), + msg="$log10 should return the base-ten log of seven", + ), + ExpressionTestCase( + "fifty", + doc={"value": 50}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(1.6989700043360187), + msg="$log10 should return the base-ten log of fifty", + ), + ExpressionTestCase( + "five_hundred", + doc={"value": 500}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(2.6989700043360187), + msg="$log10 should return the base-ten log of five hundred", + ), + ExpressionTestCase( + "five_billionth", + doc={"value": 0.000000005}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(-8.301029995663981), + msg="$log10 should return the base-ten log of five billionths", + ), + ExpressionTestCase( + "half", + doc={"value": 0.5}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(-0.3010299956639812), + msg="$log10 should return the base-ten log of one half", + ), + ExpressionTestCase( + "quarter", + doc={"value": 0.25}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(-0.6020599913279624), + msg="$log10 should return the base-ten log of one quarter", + ), + ExpressionTestCase( + "one_and_half", + doc={"value": 1.5}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(0.17609125905568124), + msg="$log10 should return the base-ten log of one and a half", + ), + ExpressionTestCase( + "two_and_half", + doc={"value": 2.5}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(0.3979400086720376), + msg="$log10 should return the base-ten log of two and a half", + ), + ExpressionTestCase( + "pi", + doc={"value": math.pi}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(0.4971498726941338), + msg="$log10 should return the base-ten log of pi", + ), + ExpressionTestCase( + "e", + doc={"value": math.e}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(0.4342944819032518), + msg="$log10 should return the base-ten log of e", + ), + ExpressionTestCase( + "e_squared", + doc={"value": math.e**2}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(0.8685889638065036), + msg="$log10 should return the base-ten log of e squared", + ), +] + +# Property [Decimal Precision]: $log10 of a decimal128 input returns a full-precision decimal128. +LOG10_DECIMAL_PRECISION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "decimal_ten", + doc={"value": Decimal128("10")}, + expression={"$log10": ["$value"]}, + expected=Decimal128("1"), + msg="$log10 should return one as a decimal128 for decimal128 ten", + ), + ExpressionTestCase( + "decimal_hundred", + doc={"value": Decimal128("100")}, + expression={"$log10": ["$value"]}, + expected=Decimal128("2"), + msg="$log10 should return two as a decimal128 for decimal128 one hundred", + ), + ExpressionTestCase( + "decimal_two", + doc={"value": Decimal128("2")}, + expression={"$log10": ["$value"]}, + expected=Decimal128("0.3010299956639811952137388947244930"), + msg="$log10 should return a full-precision decimal128 for decimal128 two", + ), +] + +LOG10_MAGNITUDE_ALL_TESTS = ( + LOG10_ONE_TESTS + + LOG10_POSITIVE_POWER_TESTS + + LOG10_NEGATIVE_POWER_TESTS + + LOG10_NON_POWER_TESTS + + LOG10_DECIMAL_PRECISION_TESTS +) + + +@pytest.mark.parametrize("test_case", pytest_params(LOG10_MAGNITUDE_ALL_TESTS)) +def test_log10_magnitude(collection, test_case: ExpressionTestCase): + """Test $log10 base-ten logarithm 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/log10/test_log10_non_finite.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_non_finite.py new file mode 100644 index 000000000..c6c4760c3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_non_finite.py @@ -0,0 +1,68 @@ +"""Tests for $log10 of positive infinity and NaN 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 +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + FLOAT_INFINITY, + FLOAT_NAN, +) + +# Property [Infinity]: $log10 of positive infinity is infinity of the same type. +LOG10_INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "float_infinity", + doc={"value": FLOAT_INFINITY}, + expression={"$log10": ["$value"]}, + expected=FLOAT_INFINITY, + msg="$log10 should return infinity for float infinity", + ), + ExpressionTestCase( + "decimal128_infinity", + doc={"value": DECIMAL128_INFINITY}, + expression={"$log10": ["$value"]}, + expected=DECIMAL128_INFINITY, + msg="$log10 should return decimal128 infinity for decimal128 infinity", + ), +] + +# Property [NaN]: $log10 of NaN returns a double NaN, including for a decimal128 NaN input. +LOG10_NAN_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "float_nan", + doc={"value": FLOAT_NAN}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$log10 should return NaN for float NaN", + ), + ExpressionTestCase( + "decimal128_nan", + doc={"value": DECIMAL128_NAN}, + expression={"$log10": ["$value"]}, + expected=pytest.approx(FLOAT_NAN, nan_ok=True), + msg="$log10 should return NaN for decimal128 NaN", + ), +] + +LOG10_NON_FINITE_TESTS = LOG10_INFINITY_TESTS + LOG10_NAN_TESTS + + +@pytest.mark.parametrize("test_case", pytest_params(LOG10_NON_FINITE_TESTS)) +def test_log10_non_finite(collection, test_case: ExpressionTestCase): + """Test $log10 positive infinity and NaN 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/log10/test_log10_null.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_null.py new file mode 100644 index 000000000..0f82b3181 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_null.py @@ -0,0 +1,43 @@ +"""Tests for $log10 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]: $log10 of null or a missing field returns null. +LOG10_NULL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_value", + doc={"value": None}, + expression={"$log10": ["$value"]}, + expected=None, + msg="$log10 should return null for null input", + ), + ExpressionTestCase( + "missing_field", + doc={}, + expression={"$log10": [MISSING]}, + expected=None, + msg="$log10 should return null for a missing field", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(LOG10_NULL_TESTS)) +def test_log10_null(collection, test_case: ExpressionTestCase): + """Test $log10 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/log10/test_log10_return_type.py b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_return_type.py new file mode 100644 index 000000000..9295b793d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/arithmetic/log10/test_log10_return_type.py @@ -0,0 +1,58 @@ +"""Tests for $log10 return type across numeric input 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]: $log10 returns double for int32, int64, and double inputs, and decimal +# for decimal128 inputs. +LOG10_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type_int32", + doc={"value": 1}, + expression={"$type": {"$log10": "$value"}}, + expected="double", + msg="$log10 should return a double for an int32 input", + ), + ExpressionTestCase( + "return_type_int64", + doc={"value": Int64(1)}, + expression={"$type": {"$log10": "$value"}}, + expected="double", + msg="$log10 should return a double for an int64 input", + ), + ExpressionTestCase( + "return_type_double", + doc={"value": 1.0}, + expression={"$type": {"$log10": "$value"}}, + expected="double", + msg="$log10 should return a double for a double input", + ), + ExpressionTestCase( + "return_type_decimal", + doc={"value": Decimal128("1")}, + expression={"$type": {"$log10": "$value"}}, + expected="decimal", + msg="$log10 should return a decimal for a decimal128 input", + ), +] + + +@pytest.mark.parametrize("test_case", pytest_params(LOG10_RETURN_TYPE_TESTS)) +def test_log10_return_type(collection, test_case: ExpressionTestCase): + """Test $log10 return type 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, + )