From f532663eb4223be2fc4a69e49db9d7f2161b674f Mon Sep 17 00:00:00 2001 From: Mehbub Rohit Date: Sun, 28 Jun 2026 20:21:57 -0400 Subject: [PATCH] Add $isNumber compatibility tests (#212) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add compatibility test coverage for the $isNumber aggregation expression operator, covering numeric BSON types (int32, int64, double, Decimal128 — including NaN/Infinity/negative zero), non-numeric BSON types, and null/missing field inputs. Each case is exercised both as a literal expression and via a document field reference. Signed-off-by: Mehbub Rohit --- .../test_isNumber_non_numeric_types.py | 109 +++++++++++ .../isNumber/test_isNumber_null_missing.py | 39 ++++ .../isNumber/test_isNumber_numeric_types.py | 173 ++++++++++++++++++ 3 files changed, 321 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_non_numeric_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_null_missing.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_numeric_types.py diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_non_numeric_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_non_numeric_types.py new file mode 100644 index 000000000..3fec1a7cf --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_non_numeric_types.py @@ -0,0 +1,109 @@ +"""Tests for $isNumber with non-numeric BSON types — all should return false.""" + +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import Any + +import pytest +from bson import Binary, Code, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + +pytestmark = pytest.mark.aggregate + + +@dataclass(frozen=True) +class IsNumberTest(BaseTestCase): + value: Any = None + + +# Property [Non-Numeric Types]: $isNumber returns false for all non-numeric BSON types. +NON_NUMERIC_TYPE_TESTS: list[IsNumberTest] = [ + # String + IsNumberTest( + "string_empty", value="", expected=False, msg="Should return false for empty string" + ), + IsNumberTest( + "string_word", value="hello", expected=False, msg="Should return false for string" + ), + IsNumberTest( + "string_numeric", value="42", expected=False, msg="Should return false for numeric string" + ), + # Boolean + IsNumberTest( + "bool_true", value=True, expected=False, msg="Should return false for boolean true" + ), + IsNumberTest( + "bool_false", value=False, expected=False, msg="Should return false for boolean false" + ), + # Array + IsNumberTest( + "array_empty", value=[], expected=False, msg="Should return false for empty array" + ), + IsNumberTest( + "array_of_numbers", + value=[1, 2, 3], + expected=False, + msg="Should return false for array of numbers", + ), + # Object + IsNumberTest( + "object_empty", value={}, expected=False, msg="Should return false for empty object" + ), + IsNumberTest( + "object_with_fields", value={"a": 1}, expected=False, msg="Should return false for object" + ), + # ObjectId + IsNumberTest( + "objectid", value=ObjectId(), expected=False, msg="Should return false for ObjectId" + ), + # Date + IsNumberTest( + "date", + value=datetime(2024, 1, 1, tzinfo=timezone.utc), + expected=False, + msg="Should return false for Date", + ), + # Timestamp + IsNumberTest( + "timestamp", value=Timestamp(1, 0), expected=False, msg="Should return false for Timestamp" + ), + # Binary + IsNumberTest( + "binary", value=Binary(b"\x00"), expected=False, msg="Should return false for Binary" + ), + # Regex + IsNumberTest("regex", value=Regex(".*"), expected=False, msg="Should return false for Regex"), + # MinKey / MaxKey + IsNumberTest("minkey", value=MinKey(), expected=False, msg="Should return false for MinKey"), + IsNumberTest("maxkey", value=MaxKey(), expected=False, msg="Should return false for MaxKey"), + # Code + IsNumberTest( + "code", + value=Code("function(){}"), + expected=False, + msg="Should return false for JavaScript Code", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(NON_NUMERIC_TYPE_TESTS)) +def test_isNumber_non_numeric_literal(collection, test): + """Test $isNumber returns false for non-numeric BSON type literals.""" + result = execute_expression(collection, {"$isNumber": test.value}) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +@pytest.mark.parametrize("test", pytest_params(NON_NUMERIC_TYPE_TESTS)) +def test_isNumber_non_numeric_field(collection, test): + """Test $isNumber returns false when referencing a document field with a non-numeric value.""" + result = execute_expression_with_insert( + collection, {"$isNumber": "$value"}, {"value": test.value} + ) + assert_expression_result(result, expected=test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_null_missing.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_null_missing.py new file mode 100644 index 000000000..23a1546e6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_null_missing.py @@ -0,0 +1,39 @@ +"""Tests for $isNumber with null and missing field inputs — both return false.""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) + +pytestmark = pytest.mark.aggregate + + +def test_isNumber_null_literal(collection): + """Test $isNumber returns false for a null literal.""" + result = execute_expression(collection, {"$isNumber": None}) + assert_expression_result(result, expected=False, msg="Should return false for null") + + +def test_isNumber_null_field(collection): + """Test $isNumber returns false when referencing a document field with a null value.""" + result = execute_expression_with_insert(collection, {"$isNumber": "$value"}, {"value": None}) + assert_expression_result( + result, expected=False, msg="Should return false for null-valued field" + ) + + +def test_isNumber_missing_field(collection): + """Test $isNumber returns false when referencing a field that does not exist in the document.""" + result = execute_expression_with_insert(collection, {"$isNumber": "$value"}, {}) + assert_expression_result(result, expected=False, msg="Should return false for missing field") + + +def test_isNumber_nested_missing_field(collection): + """Test $isNumber returns false when referencing a nested field path that does not exist.""" + result = execute_expression_with_insert(collection, {"$isNumber": "$a.b"}, {"a": {}}) + assert_expression_result( + result, expected=False, msg="Should return false for missing nested field" + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_numeric_types.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_numeric_types.py new file mode 100644 index 000000000..51a03e639 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/type/isNumber/test_isNumber_numeric_types.py @@ -0,0 +1,173 @@ +"""Tests for $isNumber with numeric BSON types (int32, int64, double, Decimal128).""" + +from dataclasses import dataclass +from typing import Any + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, + execute_expression_with_insert, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ZERO, + DOUBLE_NEGATIVE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT32_ZERO, + INT64_MAX, + INT64_MIN, + INT64_ZERO, +) + +pytestmark = pytest.mark.aggregate + + +@dataclass(frozen=True) +class IsNumberTest(BaseTestCase): + value: Any = None + + +# Property [Numeric Types]: $isNumber returns true for all numeric BSON types. +NUMERIC_TYPE_TESTS: list[IsNumberTest] = [ + # int32 + IsNumberTest( + "int32_zero", value=INT32_ZERO, expected=True, msg="Should return true for int32 zero" + ), + IsNumberTest( + "int32_positive", value=42, expected=True, msg="Should return true for positive int32" + ), + IsNumberTest( + "int32_negative", value=-1, expected=True, msg="Should return true for negative int32" + ), + IsNumberTest( + "int32_max", value=INT32_MAX, expected=True, msg="Should return true for int32 max" + ), + IsNumberTest( + "int32_min", value=INT32_MIN, expected=True, msg="Should return true for int32 min" + ), + # int64 + IsNumberTest( + "int64_zero", value=INT64_ZERO, expected=True, msg="Should return true for int64 zero" + ), + IsNumberTest( + "int64_positive", + value=Int64(100), + expected=True, + msg="Should return true for positive int64", + ), + IsNumberTest( + "int64_negative", + value=Int64(-100), + expected=True, + msg="Should return true for negative int64", + ), + IsNumberTest( + "int64_max", value=INT64_MAX, expected=True, msg="Should return true for int64 max" + ), + IsNumberTest( + "int64_min", value=INT64_MIN, expected=True, msg="Should return true for int64 min" + ), + # double + IsNumberTest("double_zero", value=0.0, expected=True, msg="Should return true for double zero"), + IsNumberTest( + "double_positive", value=3.14, expected=True, msg="Should return true for positive double" + ), + IsNumberTest( + "double_negative", value=-2.71, expected=True, msg="Should return true for negative double" + ), + IsNumberTest( + "double_negative_zero", + value=DOUBLE_NEGATIVE_ZERO, + expected=True, + msg="Should return true for double negative zero", + ), + IsNumberTest( + "double_nan", + value=FLOAT_NAN, + expected=True, + msg="Should return true for double NaN (numeric type)", + ), + IsNumberTest( + "double_infinity", + value=FLOAT_INFINITY, + expected=True, + msg="Should return true for double Infinity (numeric type)", + ), + IsNumberTest( + "double_neg_infinity", + value=FLOAT_NEGATIVE_INFINITY, + expected=True, + msg="Should return true for double negative Infinity (numeric type)", + ), + # Decimal128 + IsNumberTest( + "decimal128_zero", + value=DECIMAL128_ZERO, + expected=True, + msg="Should return true for Decimal128 zero", + ), + IsNumberTest( + "decimal128_positive", + value=Decimal128("1.5"), + expected=True, + msg="Should return true for positive Decimal128", + ), + IsNumberTest( + "decimal128_negative", + value=Decimal128("-1.5"), + expected=True, + msg="Should return true for negative Decimal128", + ), + IsNumberTest( + "decimal128_negative_zero", + value=DECIMAL128_NEGATIVE_ZERO, + expected=True, + msg="Should return true for Decimal128 negative zero", + ), + IsNumberTest( + "decimal128_nan", + value=DECIMAL128_NAN, + expected=True, + msg="Should return true for Decimal128 NaN (numeric type)", + ), + IsNumberTest( + "decimal128_infinity", + value=DECIMAL128_INFINITY, + expected=True, + msg="Should return true for Decimal128 Infinity (numeric type)", + ), + IsNumberTest( + "decimal128_neg_infinity", + value=DECIMAL128_NEGATIVE_INFINITY, + expected=True, + msg="Should return true for Decimal128 negative Infinity (numeric type)", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(NUMERIC_TYPE_TESTS)) +def test_isNumber_numeric_literal(collection, test): + """Test $isNumber returns true for numeric BSON type literals.""" + result = execute_expression(collection, {"$isNumber": test.value}) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +@pytest.mark.parametrize("test", pytest_params(NUMERIC_TYPE_TESTS)) +def test_isNumber_numeric_field(collection, test): + """Test $isNumber returns true when referencing a document field with a numeric value.""" + result = execute_expression_with_insert( + collection, {"$isNumber": "$value"}, {"value": test.value} + ) + assert_expression_result(result, expected=test.expected, msg=test.msg)