diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/__init__.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_binary_subtypes.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_binary_subtypes.py new file mode 100644 index 000000000..532958b5e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_binary_subtypes.py @@ -0,0 +1,87 @@ +"""$type binary subtype tests. + +Tests that $type returns 'binData' for all Binary subtypes regardless of +subtype number or payload content. +""" + +from __future__ import annotations + +import pytest +from bson import Binary + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Binary Subtypes]: all Binary subtypes return "binData" regardless +# of subtype or payload. +TYPE_BINARY_SUBTYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "binary_subtype_0_generic", + expression={"$type": Binary(b"test", 0)}, + expected="binData", + msg="$type should return 'binData' for Binary subtype 0 (generic)", + ), + ExpressionTestCase( + "binary_subtype_1_function", + expression={"$type": Binary(b"test", 1)}, + expected="binData", + msg="$type should return 'binData' for Binary subtype 1 (function)", + ), + ExpressionTestCase( + "binary_subtype_2_old_binary", + expression={"$type": Binary(b"test", 2)}, + expected="binData", + msg="$type should return 'binData' for Binary subtype 2 (old binary)", + ), + ExpressionTestCase( + "binary_subtype_3_old_uuid", + expression={"$type": Binary(b"\x00" * 16, 3)}, + expected="binData", + msg="$type should return 'binData' for Binary subtype 3 (old UUID)", + ), + ExpressionTestCase( + "binary_subtype_4_uuid", + expression={"$type": Binary(b"\x00" * 16, 4)}, + expected="binData", + msg="$type should return 'binData' for Binary subtype 4 (UUID)", + ), + ExpressionTestCase( + "binary_subtype_5_md5", + expression={"$type": Binary(b"\x00" * 16, 5)}, + expected="binData", + msg="$type should return 'binData' for Binary subtype 5 (MD5)", + ), + ExpressionTestCase( + "binary_subtype_6_encrypted", + expression={"$type": Binary(b"\x00" * 32, 6)}, + expected="binData", + msg="$type should return 'binData' for Binary subtype 6 (encrypted)", + ), + ExpressionTestCase( + "binary_subtype_128_user_defined", + expression={"$type": Binary(b"test", 128)}, + expected="binData", + msg="$type should return 'binData' for Binary subtype 128 (user-defined)", + ), + ExpressionTestCase( + "binary_empty", + expression={"$type": Binary(b"")}, + expected="binData", + msg="$type should return 'binData' for empty binary data", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TYPE_BINARY_SUBTYPE_TESTS)) +def test_type_binary_subtypes(collection, test: ExpressionTestCase): + """$type returns 'binData' for all Binary subtypes.""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_bson_mapping.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_bson_mapping.py new file mode 100644 index 000000000..5bae35bc6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_bson_mapping.py @@ -0,0 +1,285 @@ +"""$type BSON type mapping tests. + +Tests that $type returns the correct type-name string for each BSON type, +including null/missing, core types, array-literal unwrapping, object +expressions, system variables, and large inputs. +""" + +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest +from bson import ( + Binary, + Code, + Decimal128, + Int64, + 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, +) +from documentdb_tests.framework.lazy_payload import lazy +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DOUBLE_ONE_AND_HALF, + MISSING, + REGEX_PATTERN_LIMIT_BYTES, + STRING_SIZE_LIMIT_BYTES, +) + +# Property [Null and Missing Identification]: $type returns "null" for null +# values and "missing" for missing values, rather than propagating null. +TYPE_NULL_MISSING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_literal", + expression={"$type": None}, + expected="null", + msg="$type should return 'null' for a null literal", + ), + ExpressionTestCase( + "missing_field", + expression={"$type": MISSING}, + expected="missing", + msg="$type should return 'missing' for a reference to a missing field", + ), +] + +# Property [Core BSON Type Mapping]: each BSON type produces a specific, +# distinct type name string. +TYPE_CORE_BSON_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "core_int", + expression={"$type": 42}, + expected="int", + msg="$type should return 'int' for an int32 value", + ), + ExpressionTestCase( + "core_long", + expression={"$type": Int64(42)}, + expected="long", + msg="$type should return 'long' for an int64 value", + ), + ExpressionTestCase( + "core_double", + expression={"$type": DOUBLE_ONE_AND_HALF}, + expected="double", + msg="$type should return 'double' for a double value", + ), + ExpressionTestCase( + "core_decimal", + expression={"$type": Decimal128("16")}, + expected="decimal", + msg="$type should return 'decimal' for a Decimal128 value", + ), + ExpressionTestCase( + "core_string", + expression={"$type": "hello"}, + expected="string", + msg="$type should return 'string' for a string value", + ), + ExpressionTestCase( + "core_object", + expression={"$type": {"a": 1}}, + expected="object", + msg="$type should return 'object' for an object value", + ), + ExpressionTestCase( + "core_array", + expression={"$type": {"$literal": [1, 2, 3]}}, + expected="array", + msg="$type should return 'array' for an array value", + ), + ExpressionTestCase( + "core_bindata", + expression={"$type": Binary(b"hello")}, + expected="binData", + msg="$type should return 'binData' for a Binary value", + ), + ExpressionTestCase( + "core_objectid", + expression={"$type": ObjectId("507f1f77bcf86cd799439011")}, + expected="objectId", + msg="$type should return 'objectId' for an ObjectId value", + ), + ExpressionTestCase( + "core_bool_true", + expression={"$type": True}, + expected="bool", + msg="$type should return 'bool' for true", + ), + ExpressionTestCase( + "core_bool_false", + expression={"$type": False}, + expected="bool", + msg="$type should return 'bool' for false", + ), + ExpressionTestCase( + "core_date", + expression={"$type": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + expected="date", + msg="$type should return 'date' for a datetime value", + ), + ExpressionTestCase( + "core_regex", + expression={"$type": Regex("abc", "i")}, + expected="regex", + msg="$type should return 'regex' for a Regex value", + ), + ExpressionTestCase( + "core_javascript", + expression={"$type": Code("function(){}")}, + expected="javascript", + msg="$type should return 'javascript' for a Code value", + ), + ExpressionTestCase( + "core_timestamp", + expression={"$type": Timestamp(1, 1)}, + expected="timestamp", + msg="$type should return 'timestamp' for a Timestamp value", + ), + ExpressionTestCase( + "core_minkey", + expression={"$type": MinKey()}, + expected="minKey", + msg="$type should return 'minKey' for a MinKey value", + ), + ExpressionTestCase( + "core_maxkey", + expression={"$type": MaxKey()}, + expected="maxKey", + msg="$type should return 'maxKey' for a MaxKey value", + ), +] + +# Property [Array Literal Unwrapping]: a literal single-element array is +# unwrapped by the parser, while $literal prevents unwrapping. +TYPE_ARRAY_LITERAL_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "array_single_element_literal_unwrap", + expression={"$type": ["hello"]}, + expected="string", + msg="$type should unwrap a literal single-element array and return the inner value's type", + ), + ExpressionTestCase( + "array_literal_single_element_no_unwrap", + expression={"$type": {"$literal": ["hello"]}}, + expected="array", + msg="$type should return 'array' when $literal prevents single-element array unwrapping", + ), + ExpressionTestCase( + "array_single_element_field_path", + expression={"$type": ["$val"]}, + expected="missing", + msg="$type should evaluate ['$val'] as a field path expression, not a string literal", + ), +] + +# Property [Object Expression Handling]: plain objects passed via $literal +# return "object" regardless of key names or count. +TYPE_OBJECT_EXPRESSION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "object_empty", + expression={"$type": {"$literal": {}}}, + expected="object", + msg="$type should return 'object' for an empty object", + ), + ExpressionTestCase( + "object_non_empty", + expression={"$type": {"$literal": {"key": 1}}}, + expected="object", + msg="$type should return 'object' for a non-empty object", + ), +] + +# Property [System Variables]: system variables return the BSON type of their +# resolved value. +TYPE_SYSTEM_VARIABLE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "sysvar_root", + expression={"$type": "$$ROOT"}, + expected="object", + msg="$type should return 'object' for $$ROOT", + ), + ExpressionTestCase( + "sysvar_current", + expression={"$type": "$$CURRENT"}, + expected="object", + msg="$type should return 'object' for $$CURRENT", + ), + ExpressionTestCase( + "sysvar_now", + expression={"$type": "$$NOW"}, + expected="date", + msg="$type should return 'date' for $$NOW", + ), + ExpressionTestCase( + "sysvar_remove", + expression={"$type": "$$REMOVE"}, + expected="missing", + msg="$type should return 'missing' for $$REMOVE", + ), +] + +# Property [Large Inputs]: $type returns the correct type name for large +# values of each BSON type. +TYPE_LARGE_INPUT_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "large_string", + expression=lazy(lambda: {"$type": "a" * (STRING_SIZE_LIMIT_BYTES - 1)}), + expected="string", + msg="$type should return 'string' for a string at the size limit", + ), + ExpressionTestCase( + "large_binary", + expression=lazy(lambda: {"$type": Binary(b"\x00" * (STRING_SIZE_LIMIT_BYTES - 1))}), + expected="binData", + msg="$type should return 'binData' for binary data at the size limit", + ), + ExpressionTestCase( + "large_regex", + expression={"$type": Regex("a" * REGEX_PATTERN_LIMIT_BYTES, "i")}, + expected="regex", + msg="$type should return 'regex' for a regex pattern at the size limit", + ), + ExpressionTestCase( + "large_array", + expression={"$type": {"$literal": list(range(10_000))}}, + expected="array", + msg="$type should return 'array' for a large array", + ), + ExpressionTestCase( + "large_object", + expression=lazy(lambda: {"$type": {"$literal": {f"k{i}": i for i in range(10_000)}}}), + expected="object", + msg="$type should return 'object' for a large object", + ), +] + +TYPE_BSON_MAPPING_TESTS = ( + TYPE_NULL_MISSING_TESTS + + TYPE_CORE_BSON_TESTS + + TYPE_ARRAY_LITERAL_TESTS + + TYPE_OBJECT_EXPRESSION_TESTS + + TYPE_SYSTEM_VARIABLE_TESTS + + TYPE_LARGE_INPUT_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(TYPE_BSON_MAPPING_TESTS)) +def test_type_bson_mapping(collection, test: ExpressionTestCase): + """$type returns the correct type name for each BSON type.""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_errors.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_errors.py new file mode 100644 index 000000000..00bf0b18a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_errors.py @@ -0,0 +1,81 @@ +"""$type error handling tests. + +Tests that $type rejects invalid arity, malformed field path syntax, and +propagates errors from inner expressions. +""" + +from __future__ import annotations + +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, +) +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + EXPRESSION_TYPE_MISMATCH_ERROR, + FAILED_TO_PARSE_ERROR, + INVALID_DOLLAR_FIELD_PATH, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import INT32_ZERO + +# Property [Arity Errors]: $type takes exactly one argument; zero or multiple +# arguments produce an error. +TYPE_ARITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "arity_zero_args", + expression={"$type": []}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$type should reject zero arguments", + ), + ExpressionTestCase( + "arity_two_args", + expression={"$type": ["a", "b"]}, + error_code=EXPRESSION_TYPE_MISMATCH_ERROR, + msg="$type should reject two arguments", + ), +] + +# Property [Syntax Validation]: bare "$" and "$$" as the expression produce +# field path and variable name errors respectively. +TYPE_SYNTAX_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "syntax_bare_dollar", + expression={"$type": "$"}, + error_code=INVALID_DOLLAR_FIELD_PATH, + msg="$type should reject bare '$' as an invalid field path", + ), + ExpressionTestCase( + "syntax_bare_double_dollar", + expression={"$type": "$$"}, + error_code=FAILED_TO_PARSE_ERROR, + msg="$type should reject bare '$$' as an empty variable name", + ), +] + +# Property [Error Propagation]: when the inner expression produces an error, +# that error propagates through $type rather than returning a type name. +TYPE_ERROR_PROPAGATION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "error_prop_divide_by_zero", + expression={"$type": {"$divide": [1, INT32_ZERO]}}, + error_code=BAD_VALUE_ERROR, + msg="$type should propagate the inner expression's division-by-zero error", + ), +] + +TYPE_ERROR_TESTS = TYPE_ARITY_TESTS + TYPE_SYNTAX_TESTS + TYPE_ERROR_PROPAGATION_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TYPE_ERROR_TESTS)) +def test_type_errors(collection, test: ExpressionTestCase): + """$type rejects invalid arguments and propagates inner expression errors.""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_expressions.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_expressions.py new file mode 100644 index 000000000..1a3b8983a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_expressions.py @@ -0,0 +1,106 @@ +"""$type expression argument and return type tests. + +Tests that $type accepts any expression as its argument (returning the BSON +type of the resolved value) and always returns a value of BSON type 'string'. +""" + +from __future__ import annotations + +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, +) +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Expression Arguments]: $type accepts any expression that resolves +# to a value and returns the BSON type of the resolved result. +TYPE_EXPRESSION_ARGS_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "expr_returns_int", + expression={"$type": {"$add": [1, 2]}}, + expected="int", + msg="$type should return 'int' for an expression resolving to int", + ), + ExpressionTestCase( + "expr_returns_long", + expression={"$type": {"$add": [Int64(1), Int64(2)]}}, + expected="long", + msg="$type should return 'long' for an expression resolving to long", + ), + ExpressionTestCase( + "expr_returns_double", + expression={"$type": {"$divide": [1, 2]}}, + expected="double", + msg="$type should return 'double' for an expression resolving to double", + ), + ExpressionTestCase( + "expr_returns_decimal", + expression={"$type": {"$add": [Decimal128("1"), Decimal128("2")]}}, + expected="decimal", + msg="$type should return 'decimal' for an expression resolving to decimal", + ), + ExpressionTestCase( + "expr_returns_string", + expression={"$type": {"$concat": ["a", "b"]}}, + expected="string", + msg="$type should return 'string' for an expression resolving to string", + ), + ExpressionTestCase( + "expr_returns_bool", + expression={"$type": {"$gt": [2, 1]}}, + expected="bool", + msg="$type should return 'bool' for an expression resolving to bool", + ), + ExpressionTestCase( + "expr_returns_date", + expression={"$type": {"$dateFromString": {"dateString": "2024-01-01"}}}, + expected="date", + msg="$type should return 'date' for an expression resolving to date", + ), + ExpressionTestCase( + "expr_returns_null", + expression={"$type": {"$ifNull": [None, None]}}, + expected="null", + msg="$type should return 'null' for an expression resolving to null", + ), + ExpressionTestCase( + "expr_returns_object", + expression={"$type": {"$mergeObjects": [{"a": 1}, {"b": 2}]}}, + expected="object", + msg="$type should return 'object' for an expression resolving to object", + ), + ExpressionTestCase( + "expr_returns_array", + expression={"$type": {"$concatArrays": [[1], [2]]}}, + expected="array", + msg="$type should return 'array' for an expression resolving to array", + ), +] + +# Property [Return Type]: $type always returns a value of type "string", +# regardless of the input type. +TYPE_RETURN_TYPE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "return_type_is_string", + expression={"$type": {"$type": 42}}, + expected="string", + msg="$type should itself return a string value", + ), +] + +TYPE_EXPRESSION_TESTS = TYPE_EXPRESSION_ARGS_TESTS + TYPE_RETURN_TYPE_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TYPE_EXPRESSION_TESTS)) +def test_type_expressions(collection, test: ExpressionTestCase): + """$type returns the correct type name for values produced by expressions.""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_numeric_values.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_numeric_values.py new file mode 100644 index 000000000..d23f9704f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_numeric_values.py @@ -0,0 +1,210 @@ +"""$type numeric value tests. + +Tests that $type returns the correct BSON type name for special and boundary +numeric values, including NaN, infinities, negative zero, and type extremes. +""" + +from __future__ import annotations + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_MAX, + DECIMAL128_MAX_NEGATIVE, + DECIMAL128_MIN, + DECIMAL128_MIN_POSITIVE, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ZERO, + DOUBLE_MAX, + DOUBLE_MIN, + DOUBLE_MIN_NEGATIVE_SUBNORMAL, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT32_OVERFLOW, + INT32_UNDERFLOW, + INT64_MAX, + INT64_MIN, +) + +# Property [Special Numeric Values]: special float and Decimal128 values +# (NaN, infinity, negative zero) return their base numeric type name. +TYPE_SPECIAL_NUMERIC_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "special_float_nan", + expression={"$type": FLOAT_NAN}, + expected="double", + msg="$type should return 'double' for float NaN", + ), + ExpressionTestCase( + "special_float_inf", + expression={"$type": FLOAT_INFINITY}, + expected="double", + msg="$type should return 'double' for float infinity", + ), + ExpressionTestCase( + "special_float_neg_inf", + expression={"$type": FLOAT_NEGATIVE_INFINITY}, + expected="double", + msg="$type should return 'double' for float negative infinity", + ), + ExpressionTestCase( + "special_float_neg_zero", + expression={"$type": DOUBLE_NEGATIVE_ZERO}, + expected="double", + msg="$type should return 'double' for float negative zero", + ), + ExpressionTestCase( + "special_float_zero", + expression={"$type": DOUBLE_ZERO}, + expected="double", + msg="$type should return 'double' for float positive zero", + ), + ExpressionTestCase( + "special_decimal_nan", + expression={"$type": DECIMAL128_NAN}, + expected="decimal", + msg="$type should return 'decimal' for Decimal128 NaN", + ), + ExpressionTestCase( + "special_decimal_inf", + expression={"$type": DECIMAL128_INFINITY}, + expected="decimal", + msg="$type should return 'decimal' for Decimal128 Infinity", + ), + ExpressionTestCase( + "special_decimal_neg_inf", + expression={"$type": DECIMAL128_NEGATIVE_INFINITY}, + expected="decimal", + msg="$type should return 'decimal' for Decimal128 negative Infinity", + ), + ExpressionTestCase( + "special_decimal_neg_zero", + expression={"$type": DECIMAL128_NEGATIVE_ZERO}, + expected="decimal", + msg="$type should return 'decimal' for Decimal128 negative zero", + ), + ExpressionTestCase( + "special_decimal_zero", + expression={"$type": DECIMAL128_ZERO}, + expected="decimal", + msg="$type should return 'decimal' for Decimal128 positive zero", + ), +] + +# Property [Numeric Boundary Values]: numeric types at their boundary values +# still return the correct BSON type name. +TYPE_NUMERIC_BOUNDARY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "boundary_int32_max", + expression={"$type": INT32_MAX}, + expected="int", + msg="$type should return 'int' for int32 max value", + ), + ExpressionTestCase( + "boundary_int32_min", + expression={"$type": INT32_MIN}, + expected="int", + msg="$type should return 'int' for int32 min value", + ), + ExpressionTestCase( + "boundary_int64_max", + expression={"$type": INT64_MAX}, + expected="long", + msg="$type should return 'long' for int64 max value", + ), + ExpressionTestCase( + "boundary_int64_min", + expression={"$type": INT64_MIN}, + expected="long", + msg="$type should return 'long' for int64 min value", + ), + ExpressionTestCase( + "boundary_int32_overflow", + expression={"$type": Int64(INT32_OVERFLOW)}, + expected="long", + msg="$type should return 'long' for int32 overflow value as int64", + ), + ExpressionTestCase( + "boundary_int32_underflow", + expression={"$type": Int64(INT32_UNDERFLOW)}, + expected="long", + msg="$type should return 'long' for int32 underflow value as int64", + ), + ExpressionTestCase( + "boundary_double_max", + expression={"$type": DOUBLE_MAX}, + expected="double", + msg="$type should return 'double' for the maximum double value", + ), + ExpressionTestCase( + "boundary_double_min", + expression={"$type": DOUBLE_MIN}, + expected="double", + msg="$type should return 'double' for the minimum double value", + ), + ExpressionTestCase( + "boundary_double_min_subnormal", + expression={"$type": DOUBLE_MIN_SUBNORMAL}, + expected="double", + msg="$type should return 'double' for the smallest positive subnormal double", + ), + ExpressionTestCase( + "boundary_double_min_neg_subnormal", + expression={"$type": DOUBLE_MIN_NEGATIVE_SUBNORMAL}, + expected="double", + msg="$type should return 'double' for the smallest negative subnormal double", + ), + ExpressionTestCase( + "boundary_decimal_max", + expression={"$type": DECIMAL128_MAX}, + expected="decimal", + msg="$type should return 'decimal' for Decimal128 at max exponent", + ), + ExpressionTestCase( + "boundary_decimal_min", + expression={"$type": DECIMAL128_MIN}, + expected="decimal", + msg="$type should return 'decimal' for Decimal128 at min exponent", + ), + ExpressionTestCase( + "boundary_decimal_min_positive", + expression={"$type": DECIMAL128_MIN_POSITIVE}, + expected="decimal", + msg="$type should return 'decimal' for the smallest positive Decimal128", + ), + ExpressionTestCase( + "boundary_decimal_max_negative", + expression={"$type": DECIMAL128_MAX_NEGATIVE}, + expected="decimal", + msg="$type should return 'decimal' for the largest negative Decimal128", + ), +] + +TYPE_NUMERIC_VALUE_TESTS = TYPE_SPECIAL_NUMERIC_TESTS + TYPE_NUMERIC_BOUNDARY_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TYPE_NUMERIC_VALUE_TESTS)) +def test_type_numeric_values(collection, test: ExpressionTestCase): + """$type returns the correct type name for special and boundary numeric values.""" + result = execute_expression(collection, test.expression) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_stored.py b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_stored.py new file mode 100644 index 000000000..b722cdb7f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/type/type/test_type_stored.py @@ -0,0 +1,118 @@ +"""$type stored document field reference tests. + +Tests that $type correctly identifies the BSON type of values stored in +document fields, including arrays accessed via field references and documents +with dollar-prefixed keys. +""" + +from __future__ import annotations + +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 [Array Field References]: $type returns "array" for any array +# accessed via a field reference, regardless of contents, length, or dotted +# path traversal. +TYPE_ARRAY_FIELD_REF_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "array_basic", + expression={"$type": "$val"}, + doc={"val": [1, 2, 3]}, + expected="array", + msg="$type should return 'array' for a stored array", + ), + ExpressionTestCase( + "array_with_null", + expression={"$type": "$val"}, + doc={"val": [None, 1]}, + expected="array", + msg="$type should return 'array' for a stored array containing null", + ), + ExpressionTestCase( + "array_empty", + expression={"$type": "$val"}, + doc={"val": []}, + expected="array", + msg="$type should return 'array' for a stored empty array", + ), + ExpressionTestCase( + "array_single_element", + expression={"$type": "$val"}, + doc={"val": [42]}, + expected="array", + msg="$type should return 'array' for a stored single-element array", + ), + ExpressionTestCase( + "array_dotted_index", + expression={"$type": "$nested.0"}, + doc={"nested": [{"y": 1}, {"y": 2}]}, + expected="array", + msg="$type should return 'array' for a dotted index into an array", + ), + ExpressionTestCase( + "array_dotted_field", + expression={"$type": "$nested.y"}, + doc={"nested": [{"y": 1}, {"y": 2}]}, + expected="array", + msg="$type should return 'array' for a dotted field across array elements", + ), + ExpressionTestCase( + "array_dotted_deep", + expression={"$type": "$nested.0.y"}, + doc={"nested": [{"y": 1}, {"y": 2}]}, + expected="array", + msg="$type should return 'array' for a deep dotted path into an array", + ), + ExpressionTestCase( + "array_wrapped_field_path", + expression={"$type": ["$val"]}, + doc={"val": 42}, + expected="int", + msg="$type should resolve the field path in ['$val'] to the stored field's type", + ), +] + +# Property [Stored Operator-Like Keys]: documents with $-prefixed keys stored +# in the database are treated as plain objects, not evaluated as expressions. +TYPE_STORED_OPERATOR_KEY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "stored_operator_like_add", + expression={"$type": "$val"}, + doc={"val": {"$add": [1, 2]}}, + expected="object", + msg="$type should return 'object' for a stored document with $add key", + ), + ExpressionTestCase( + "stored_operator_like_type", + expression={"$type": "$val"}, + doc={"val": {"$type": "hello"}}, + expected="object", + msg="$type should return 'object' for a stored document with $type key", + ), + ExpressionTestCase( + "stored_operator_like_literal", + expression={"$type": "$val"}, + doc={"val": {"$literal": 1}}, + expected="object", + msg="$type should return 'object' for a stored document with $literal key", + ), +] + +TYPE_STORED_TESTS = TYPE_ARRAY_FIELD_REF_TESTS + TYPE_STORED_OPERATOR_KEY_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TYPE_STORED_TESTS)) +def test_type_stored(collection, test: ExpressionTestCase): + """$type returns the correct type name for values stored in document fields.""" + result = execute_expression_with_insert(collection, test.expression, test.doc) + assert_expression_result( + result, expected=test.expected, error_code=test.error_code, msg=test.msg + )