From 7e107b5fbe268145adfca7b3ad7b87d150b8abf6 Mon Sep 17 00:00:00 2001 From: Daniel Frankcom Date: Mon, 1 Jun 2026 10:24:03 -0700 Subject: [PATCH] Add bson types testing Signed-off-by: Daniel Frankcom --- .../bson_types/test_bson_types_ordering.py | 534 ------------------ .../{ => data-types}/bson_types/__init__.py | 0 .../bson_types/test_bson_types_no_coercion.py | 70 +++ .../bson_types/test_bson_types_ordering.py | 178 ++++++ .../test_smoke_bson_types.py} | 0 .../data-types/bson_types/types/__init__.py | 0 .../bson_types/types/test_types_array.py | 101 ++++ .../bson_types/types/test_types_binary.py | 132 +++++ .../bson_types/types/test_types_bool.py | 64 +++ .../bson_types/types/test_types_code.py | 95 ++++ .../bson_types/types/test_types_date.py | 153 +++++ .../types/test_types_minkey_maxkey.py | 77 +++ .../bson_types/types/test_types_null.py | 69 +++ .../bson_types/types/test_types_numeric.py | 503 +++++++++++++++++ .../bson_types/types/test_types_object.py | 108 ++++ .../bson_types/types/test_types_objectid.py | 122 ++++ .../bson_types/types/test_types_regex.py | 89 +++ .../bson_types/types/test_types_string.py | 181 ++++++ .../bson_types/types/test_types_timestamp.py | 135 +++++ .../data-types/bson_types/utils/__init__.py | 0 .../bson_types/utils/round_trip_test_case.py | 19 + .../eq/test_eq_boundary_precision.py | 2 +- .../eq/test_eq_numeric_edge_cases.py | 2 +- .../eq/test_eq_same_type_comparisons.py | 2 +- .../ne/test_ne_boundary_precision.py | 2 +- .../ne/test_ne_numeric_edge_cases.py | 2 +- .../ne/test_ne_same_type_comparisons.py | 2 +- 27 files changed, 2102 insertions(+), 540 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/bson_types/test_bson_types_ordering.py rename documentdb_tests/compatibility/tests/core/{ => data-types}/bson_types/__init__.py (100%) create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/test_bson_types_no_coercion.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/test_bson_types_ordering.py rename documentdb_tests/compatibility/tests/core/data-types/{bson-types/test_smoke_bson-types.py => bson_types/test_smoke_bson_types.py} (100%) create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_array.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_binary.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_bool.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_code.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_date.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_minkey_maxkey.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_null.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_numeric.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_object.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_objectid.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_regex.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_string.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_timestamp.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/data-types/bson_types/utils/round_trip_test_case.py diff --git a/documentdb_tests/compatibility/tests/core/bson_types/test_bson_types_ordering.py b/documentdb_tests/compatibility/tests/core/bson_types/test_bson_types_ordering.py deleted file mode 100644 index c736de26c..000000000 --- a/documentdb_tests/compatibility/tests/core/bson_types/test_bson_types_ordering.py +++ /dev/null @@ -1,534 +0,0 @@ -""" -Tests for BSON type ordering and comparison engine behaviour. - -Covers BSON type ordering (MinKey < Null < Number < String < ... < MaxKey), -string comparison semantics (Unicode NFC/NFD, null bytes, byte-level), -numeric equivalence across types (int == long == double == decimal128), -negative zero == positive zero, NaN == NaN (BSON-level equality), -Decimal128 precision (trailing zeros, scientific notation), -and field path resolution (nested, missing fields). -""" - -from datetime import datetime, timezone - -import pytest -from bson import Binary, 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, - execute_expression_with_insert, -) -from documentdb_tests.framework.parametrize import pytest_params -from documentdb_tests.framework.test_constants import ( - DECIMAL128_NAN, - DECIMAL128_NEGATIVE_NAN, - DECIMAL128_NEGATIVE_ZERO, - DECIMAL128_ZERO, - DOUBLE_NEGATIVE_ZERO, - FLOAT_NAN, - FLOAT_NEGATIVE_NAN, -) - -ADJACENT_TYPE_ORDERING_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "minkey_lt_null", - expression={"$lt": [MinKey(), None]}, - expected=True, - msg="MinKey < null", - ), - ExpressionTestCase( - "null_lt_int", - expression={"$lt": [None, 0]}, - expected=True, - msg="Null < int", - ), - ExpressionTestCase( - "null_lt_long", - expression={"$lt": [None, Int64(0)]}, - expected=True, - msg="Null < long", - ), - ExpressionTestCase( - "null_lt_double", - expression={"$lt": [None, 0.0]}, - expected=True, - msg="Null < double", - ), - ExpressionTestCase( - "null_lt_decimal128", - expression={"$lt": [None, Decimal128("0")]}, - expected=True, - msg="Null < decimal128", - ), - ExpressionTestCase( - "int_lt_string", - expression={"$lt": [0, ""]}, - expected=True, - msg="Int < string", - ), - ExpressionTestCase( - "long_lt_string", - expression={"$lt": [Int64(0), ""]}, - expected=True, - msg="Long < string", - ), - ExpressionTestCase( - "double_lt_string", - expression={"$lt": [0.0, ""]}, - expected=True, - msg="Double < string", - ), - ExpressionTestCase( - "decimal128_lt_string", - expression={"$lt": [Decimal128("0"), ""]}, - expected=True, - msg="Decimal128 < string", - ), - ExpressionTestCase( - "string_lt_object", - expression={"$lt": ["", {}]}, - expected=True, - msg="String < object", - ), - ExpressionTestCase( - "object_lt_array", - expression={"$lt": [{}, []]}, - expected=True, - msg="Object < array", - ), - ExpressionTestCase( - "array_lt_bindata", - expression={"$lt": [[], Binary(b"", 0)]}, - expected=True, - msg="Array < BinData", - ), - ExpressionTestCase( - "bindata_lt_objectid", - expression={"$lt": [Binary(b"", 0), ObjectId("000000000000000000000000")]}, - expected=True, - msg="BinData < ObjectId", - ), - ExpressionTestCase( - "objectid_lt_bool", - expression={"$lt": [ObjectId("000000000000000000000000"), False]}, - expected=True, - msg="ObjectId < bool", - ), - ExpressionTestCase( - "bool_lt_date", - expression={"$lt": [False, datetime(1970, 1, 1, tzinfo=timezone.utc)]}, - expected=True, - msg="Bool < date", - ), - ExpressionTestCase( - "date_lt_timestamp", - expression={"$lt": [datetime(1970, 1, 1, tzinfo=timezone.utc), Timestamp(0, 0)]}, - expected=True, - msg="Date < Timestamp", - ), - ExpressionTestCase( - "timestamp_lt_regex", - expression={"$lt": [Timestamp(0, 0), Regex("a")]}, - expected=True, - msg="Timestamp < regex", - ), - ExpressionTestCase( - "regex_lt_maxkey", - expression={"$lt": [Regex("a"), MaxKey()]}, - expected=True, - msg="Regex < MaxKey", - ), -] - - -@pytest.mark.parametrize("test", pytest_params(ADJACENT_TYPE_ORDERING_TESTS)) -def test_adjacent_type_ordering(collection, test): - """Test BSON type ordering via $lt: MinKey < Null < Number < String < ... < MaxKey.""" - result = execute_expression(collection, test.expression) - assert_expression_result(result, expected=test.expected, msg=test.msg) - - -NON_ADJACENT_TYPE_ORDERING_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "null_lt_object", - expression={"$lt": [None, {}]}, - expected=True, - msg="Null < object (skipping number and string)", - ), - ExpressionTestCase( - "int_lt_bindata", - expression={"$lt": [0, Binary(b"", 0)]}, - expected=True, - msg="Number < BinData (skipping string, object, array)", - ), - ExpressionTestCase( - "int_ne_string", - expression={"$eq": [1, "1"]}, - expected=False, - msg="Number not equal to string (no type coercion)", - ), - ExpressionTestCase( - "int_ne_bool", - expression={"$eq": [0, False]}, - expected=False, - msg="Number not equal to bool (no type coercion)", - ), - ExpressionTestCase( - "null_ne_bool", - expression={"$eq": [None, False]}, - expected=False, - msg="Null not equal to bool (no type coercion)", - ), - ExpressionTestCase( - "bool_ne_int", - expression={"$eq": [True, 1]}, - expected=False, - msg="Bool not equal to number (no type coercion)", - ), - ExpressionTestCase( - "empty_string_ne_none", - expression={"$eq": ["", None]}, - expected=False, - msg="Empty string not equal to null (no null coercion)", - ), - ExpressionTestCase( - "zero_ne_none", - expression={"$eq": [0, None]}, - expected=False, - msg="Zero not equal to null (no null coercion)", - ), - ExpressionTestCase( - "empty_string_ne_false", - expression={"$eq": ["", False]}, - expected=False, - msg="Empty string not equal to false (no falsy coercion)", - ), - ExpressionTestCase( - "empty_array_ne_false", - expression={"$eq": [[], False]}, - expected=False, - msg="Empty array not equal to false (no falsy coercion)", - ), - ExpressionTestCase( - "string_lt_timestamp", - expression={"$lt": ["", Timestamp(0, 0)]}, - expected=True, - msg="String < Timestamp (skipping object, array, bindata, ObjectId, bool, date)", - ), -] - - -@pytest.mark.parametrize("test", pytest_params(NON_ADJACENT_TYPE_ORDERING_TESTS)) -def test_non_adjacent_type_ordering(collection, test): - """Test non-adjacent cross-type comparisons for transitive ordering.""" - result = execute_expression(collection, test.expression) - assert_expression_result(result, expected=test.expected, msg=test.msg) - - -STRING_COMPARISON_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "str_equal", expression={"$eq": ["abc", "abc"]}, expected=True, msg="Same strings equal" - ), - ExpressionTestCase( - "str_case_sensitive", - expression={"$eq": ["abc", "ABC"]}, - expected=False, - msg="Case-sensitive comparison", - ), - ExpressionTestCase( - "str_different", - expression={"$eq": ["abc", "abd"]}, - expected=False, - msg="Different strings not equal", - ), - ExpressionTestCase( - "str_empty", expression={"$eq": ["", ""]}, expected=True, msg="Empty strings equal" - ), - ExpressionTestCase( - "str_diff_length", - expression={"$eq": ["abc", "abcd"]}, - expected=False, - msg="Different length not equal", - ), - ExpressionTestCase( - "str_null_byte", - expression={"$eq": ["abc\0", "abc"]}, - expected=False, - msg="String with null byte differs", - ), - ExpressionTestCase( - "str_nfc_vs_nfd", - expression={"$eq": ["caf\u00e9", "cafe\u0301"]}, - expected=False, - msg="NFC and NFD normalization forms are not equal (byte-level comparison)", - ), - ExpressionTestCase( - "str_emoji", - expression={"$eq": ["\U0001f600", "\U0001f600"]}, - expected=True, - msg="Same emoji equal", - ), - ExpressionTestCase( - "str_cjk", - expression={"$eq": ["\u4e16\u754c", "\u4e16\u754c"]}, - expected=True, - msg="Same CJK equal", - ), - ExpressionTestCase( - "str_cjk_diff", - expression={"$eq": ["\u4e16\u754c", "\u4e16\u754d"]}, - expected=False, - msg="Different CJK", - ), - ExpressionTestCase( - "regex_flag_order", - expression={"$eq": [Regex("abc", "im"), Regex("abc", "mi")]}, - expected=True, - msg="Regex flags are normalized — order does not matter", - ), -] - -NUMERIC_EQUIVALENCE_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "num_int_long", - expression={"$eq": [1, Int64(1)]}, - expected=True, - msg="Int32(1) equals Int64(1)", - ), - ExpressionTestCase( - "num_int_double", - expression={"$eq": [1, 1.0]}, - expected=True, - msg="Int32(1) equals Double(1.0)", - ), - ExpressionTestCase( - "num_int_dec128", - expression={"$eq": [1, Decimal128("1")]}, - expected=True, - msg="Int32(1) equals Decimal128(1)", - ), - ExpressionTestCase( - "num_long_double", - expression={"$eq": [Int64(1), 1.0]}, - expected=True, - msg="Int64(1) equals Double(1.0)", - ), - ExpressionTestCase( - "num_long_dec128", - expression={"$eq": [Int64(1), Decimal128("1")]}, - expected=True, - msg="Int64(1) equals Decimal128(1)", - ), - ExpressionTestCase( - "num_double_dec128", - expression={"$eq": [1.0, Decimal128("1")]}, - expected=True, - msg="Double(1.0) equals Decimal128(1)", - ), - ExpressionTestCase( - "num_zero_int_long", - expression={"$eq": [0, Int64(0)]}, - expected=True, - msg="Int32(0) equals Int64(0)", - ), - ExpressionTestCase( - "num_zero_int_double", - expression={"$eq": [0, 0.0]}, - expected=True, - msg="Int32(0) equals Double(0.0)", - ), - ExpressionTestCase( - "num_zero_double_dec128", - expression={"$eq": [0.0, Decimal128("0")]}, - expected=True, - msg="Double(0.0) equals Decimal128(0)", - ), -] - -NEGATIVE_ZERO_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "negzero_double", - expression={"$eq": [DOUBLE_NEGATIVE_ZERO, 0.0]}, - expected=True, - msg="-0.0 equals 0.0", - ), - ExpressionTestCase( - "negzero_dec128", - expression={"$eq": [DECIMAL128_NEGATIVE_ZERO, DECIMAL128_ZERO]}, - expected=True, - msg="Decimal128(-0) equals Decimal128(0)", - ), - ExpressionTestCase( - "negzero_int", - expression={"$eq": [DOUBLE_NEGATIVE_ZERO, 0]}, - expected=True, - msg="-0.0 equals Int32(0)", - ), - ExpressionTestCase( - "negzero_long", - expression={"$eq": [DOUBLE_NEGATIVE_ZERO, Int64(0)]}, - expected=True, - msg="-0.0 equals Int64(0)", - ), - ExpressionTestCase( - "negzero_cross_dec128", - expression={"$eq": [DOUBLE_NEGATIVE_ZERO, Decimal128("0")]}, - expected=True, - msg="-0.0 equals Decimal128(0)", - ), -] - -NAN_EQUALITY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "nan_nan", expression={"$eq": [FLOAT_NAN, FLOAT_NAN]}, expected=True, msg="NaN equals NaN" - ), - ExpressionTestCase( - "nan_negative_nan", - expression={"$eq": [FLOAT_NAN, FLOAT_NEGATIVE_NAN]}, - expected=True, - msg="NaN equals NaN", - ), - ExpressionTestCase( - "nan_int", expression={"$eq": [FLOAT_NAN, 1]}, expected=False, msg="NaN not equal to int" - ), - ExpressionTestCase( - "nan_null", - expression={"$eq": [FLOAT_NAN, None]}, - expected=False, - msg="NaN not equal to null", - ), - ExpressionTestCase( - "nan_dec_nan", - expression={"$eq": [DECIMAL128_NAN, DECIMAL128_NAN]}, - expected=True, - msg="Decimal128 NaN equals Decimal128 NaN", - ), - ExpressionTestCase( - "dec_nan_negative_dec_nan", - expression={"$eq": [DECIMAL128_NAN, DECIMAL128_NEGATIVE_NAN]}, - expected=True, - msg="Decimal128 NaN equals Decimal128 NaN", - ), - ExpressionTestCase( - "nan_cross_type", - expression={"$eq": [FLOAT_NAN, DECIMAL128_NAN]}, - expected=True, - msg="Cross-type NaN equality", - ), -] - -DECIMAL128_PRECISION_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "dec_trailing_zeros", - expression={"$eq": [Decimal128("1.0"), Decimal128("1.00")]}, - expected=True, - msg="Trailing zeros are equal", - ), - ExpressionTestCase( - "dec_scientific", - expression={"$eq": [Decimal128("1E+2"), Decimal128("100")]}, - expected=True, - msg="Scientific notation equals standard", - ), - ExpressionTestCase( - "dec_zero_diff_exp", - expression={"$eq": [Decimal128("0E-6176"), Decimal128("0")]}, - expected=True, - msg="Zero with different exponents are equal", - ), - ExpressionTestCase( - "dec_zero_extreme_exp", - expression={"$eq": [Decimal128("0E-6176"), Decimal128("0E+6111")]}, - expected=True, - msg="Zero with extreme exponent range are equal", - ), -] - -MINKEY_MAXKEY_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "minkey_eq_minkey", - expression={"$eq": [MinKey(), MinKey()]}, - expected=True, - msg="MinKey equals MinKey", - ), - ExpressionTestCase( - "maxkey_eq_maxkey", - expression={"$eq": [MaxKey(), MaxKey()]}, - expected=True, - msg="MaxKey equals MaxKey", - ), - ExpressionTestCase( - "minkey_ne_maxkey", - expression={"$eq": [MinKey(), MaxKey()]}, - expected=False, - msg="MinKey not equal to MaxKey", - ), -] - -ALL_LITERAL_TESTS = ( - STRING_COMPARISON_TESTS - + NUMERIC_EQUIVALENCE_TESTS - + NEGATIVE_ZERO_TESTS - + NAN_EQUALITY_TESTS - + DECIMAL128_PRECISION_TESTS - + MINKEY_MAXKEY_TESTS -) - - -@pytest.mark.parametrize("test", pytest_params(ALL_LITERAL_TESTS)) -def test_bson_comparison_engine(collection, test): - """Test BSON comparison engine: strings, numerics, NaN, Decimal128.""" - result = execute_expression(collection, test.expression) - assert_expression_result(result, expected=test.expected, msg=test.msg) - - -FIELD_PATH_TESTS: list[ExpressionTestCase] = [ - ExpressionTestCase( - "fp_simple", - expression={"$eq": ["$a", 1]}, - doc={"a": 1}, - expected=True, - msg="Simple field lookup", - ), - ExpressionTestCase( - "fp_nested", - expression={"$eq": ["$a.b", 1]}, - doc={"a": {"b": 1}}, - expected=True, - msg="Nested field lookup", - ), - ExpressionTestCase( - "fp_deep", - expression={"$eq": ["$a.b.c.d.e", 1]}, - doc={"a": {"b": {"c": {"d": {"e": 1}}}}}, - expected=True, - msg="Deeply nested field lookup", - ), - ExpressionTestCase( - "fp_missing_vs_null", - expression={"$eq": ["$missing", None]}, - doc={"a": 1}, - expected=False, - msg="Missing field not equal to null literal", - ), - ExpressionTestCase( - "fp_missing_nested", - expression={"$eq": ["$x.y.z", None]}, - doc={}, - expected=False, - msg="Missing nested field not equal to null literal", - ), -] - -ALL_INSERT_TESTS = FIELD_PATH_TESTS - - -@pytest.mark.parametrize("test", pytest_params(ALL_INSERT_TESTS)) -def test_bson_field_path_resolution(collection, test): - """Test field path resolution: nested, missing fields.""" - result = execute_expression_with_insert(collection, test.expression, test.doc) - assert_expression_result(result, expected=test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/bson_types/__init__.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/__init__.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/bson_types/__init__.py rename to documentdb_tests/compatibility/tests/core/data-types/bson_types/__init__.py diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/test_bson_types_no_coercion.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/test_bson_types_no_coercion.py new file mode 100644 index 000000000..7f6a8c461 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/test_bson_types_no_coercion.py @@ -0,0 +1,70 @@ +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.parametrize import pytest_params + +# Property [No Cross-Type Coercion]: BSON equality does not implicitly coerce +# values across types. +NO_COERCION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int_ne_string", + expression={"$eq": [1, "1"]}, + expected=False, + msg="BSON equality should not coerce a number to a string", + ), + ExpressionTestCase( + "int_ne_bool", + expression={"$eq": [0, False]}, + expected=False, + msg="BSON equality should not coerce a number to a bool", + ), + ExpressionTestCase( + "one_ne_true", + expression={"$eq": [1, True]}, + expected=False, + msg="BSON equality should not coerce a number to a bool", + ), + ExpressionTestCase( + "null_ne_bool", + expression={"$eq": [None, False]}, + expected=False, + msg="BSON equality should not coerce null to a bool", + ), + ExpressionTestCase( + "empty_string_ne_null", + expression={"$eq": ["", None]}, + expected=False, + msg="BSON equality should not coerce an empty string to null", + ), + ExpressionTestCase( + "zero_ne_null", + expression={"$eq": [0, None]}, + expected=False, + msg="BSON equality should not coerce zero to null", + ), + ExpressionTestCase( + "empty_string_ne_false", + expression={"$eq": ["", False]}, + expected=False, + msg="BSON equality should not coerce an empty string to false", + ), + ExpressionTestCase( + "empty_array_ne_false", + expression={"$eq": [[], False]}, + expected=False, + msg="BSON equality should not coerce an empty array to false", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(NO_COERCION_TESTS)) +def test_bson_types_no_coercion(collection, test): + """Test BSON equality does not implicitly coerce across types.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/test_bson_types_ordering.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/test_bson_types_ordering.py new file mode 100644 index 000000000..7b10a9f56 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/test_bson_types_ordering.py @@ -0,0 +1,178 @@ +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, +) +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_ZERO, + DOUBLE_ZERO, + INT64_ZERO, +) + +# Property [Adjacent Type Order]: each BSON type sorts immediately before the +# next type in the canonical order MinKey < Null < Number < String < Object < +# Array < BinData < ObjectId < Bool < Date < Timestamp < Regex < JavaScript < +# MaxKey. +ADJACENT_TYPE_ORDERING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "minkey_lt_null", + expression={"$lt": [MinKey(), None]}, + expected=True, + msg="BSON ordering should place MinKey before null", + ), + ExpressionTestCase( + "null_lt_int", + expression={"$lt": [None, 0]}, + expected=True, + msg="BSON ordering should place null before int32", + ), + ExpressionTestCase( + "null_lt_long", + expression={"$lt": [None, INT64_ZERO]}, + expected=True, + msg="BSON ordering should place null before Int64", + ), + ExpressionTestCase( + "null_lt_double", + expression={"$lt": [None, DOUBLE_ZERO]}, + expected=True, + msg="BSON ordering should place null before double", + ), + ExpressionTestCase( + "null_lt_decimal128", + expression={"$lt": [None, DECIMAL128_ZERO]}, + expected=True, + msg="BSON ordering should place null before Decimal128", + ), + ExpressionTestCase( + "int_lt_string", + expression={"$lt": [0, ""]}, + expected=True, + msg="BSON ordering should place int32 before string", + ), + ExpressionTestCase( + "long_lt_string", + expression={"$lt": [INT64_ZERO, ""]}, + expected=True, + msg="BSON ordering should place Int64 before string", + ), + ExpressionTestCase( + "double_lt_string", + expression={"$lt": [DOUBLE_ZERO, ""]}, + expected=True, + msg="BSON ordering should place double before string", + ), + ExpressionTestCase( + "decimal128_lt_string", + expression={"$lt": [DECIMAL128_ZERO, ""]}, + expected=True, + msg="BSON ordering should place Decimal128 before string", + ), + ExpressionTestCase( + "string_lt_object", + expression={"$lt": ["", {}]}, + expected=True, + msg="BSON ordering should place string before object", + ), + ExpressionTestCase( + "object_lt_array", + expression={"$lt": [{}, []]}, + expected=True, + msg="BSON ordering should place object before array", + ), + ExpressionTestCase( + "array_lt_bindata", + expression={"$lt": [[], Binary(b"", 0)]}, + expected=True, + msg="BSON ordering should place array before BinData", + ), + ExpressionTestCase( + "bindata_lt_objectid", + expression={"$lt": [Binary(b"", 0), ObjectId("000000000000000000000000")]}, + expected=True, + msg="BSON ordering should place BinData before ObjectId", + ), + ExpressionTestCase( + "objectid_lt_bool", + expression={"$lt": [ObjectId("000000000000000000000000"), False]}, + expected=True, + msg="BSON ordering should place ObjectId before bool", + ), + ExpressionTestCase( + "bool_lt_date", + expression={"$lt": [False, datetime(1970, 1, 1, tzinfo=timezone.utc)]}, + expected=True, + msg="BSON ordering should place bool before date", + ), + ExpressionTestCase( + "date_lt_timestamp", + expression={"$lt": [datetime(1970, 1, 1, tzinfo=timezone.utc), Timestamp(0, 0)]}, + expected=True, + msg="BSON ordering should place date before Timestamp", + ), + ExpressionTestCase( + "timestamp_lt_regex", + expression={"$lt": [Timestamp(0, 0), Regex("a")]}, + expected=True, + msg="BSON ordering should place Timestamp before regex", + ), + ExpressionTestCase( + "regex_lt_javascript", + expression={"$lt": [Regex("a"), Code("f")]}, + expected=True, + msg="BSON ordering should place regex before JavaScript code", + ), + ExpressionTestCase( + "javascript_lt_maxkey", + expression={"$lt": [Code("f"), MaxKey()]}, + expected=True, + msg="BSON ordering should place JavaScript code before MaxKey", + ), +] + +# Property [Non-Adjacent Type Order]: cross-type ordering is transitive, so a +# type sorts before every type that appears later in the canonical order, not +# only its immediate successor. +NON_ADJACENT_TYPE_ORDERING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_lt_object", + expression={"$lt": [None, {}]}, + expected=True, + msg="BSON ordering should place null before object across intervening types", + ), + ExpressionTestCase( + "int_lt_bindata", + expression={"$lt": [0, Binary(b"", 0)]}, + expected=True, + msg="BSON ordering should place number before BinData across intervening types", + ), + ExpressionTestCase( + "string_lt_timestamp", + expression={"$lt": ["", Timestamp(0, 0)]}, + expected=True, + msg="BSON ordering should place string before Timestamp across intervening types", + ), + ExpressionTestCase( + "minkey_lt_maxkey", + expression={"$lt": [MinKey(), MaxKey()]}, + expected=True, + msg="BSON ordering should place MinKey before MaxKey at the extremes", + ), +] + +TYPE_ORDERING_TESTS = ADJACENT_TYPE_ORDERING_TESTS + NON_ADJACENT_TYPE_ORDERING_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TYPE_ORDERING_TESTS)) +def test_bson_types_ordering(collection, test): + """Test cross-type BSON comparison order.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson-types/test_smoke_bson-types.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/test_smoke_bson_types.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/data-types/bson-types/test_smoke_bson-types.py rename to documentdb_tests/compatibility/tests/core/data-types/bson_types/test_smoke_bson_types.py diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/__init__.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_array.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_array.py new file mode 100644 index 000000000..16e754989 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_array.py @@ -0,0 +1,101 @@ +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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [Array Ordering]: arrays compare element by element, with a prefix +# array sorting before a longer one. +ARRAY_COMPARISON_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty_equality", + expression={"$eq": [[], []]}, + expected=True, + msg="Array equality should hold for two empty arrays", + ), + ExpressionTestCase( + "element_by_element", + expression={"$lt": [[1, 2], [2]]}, + expected=True, + msg="Array ordering should compare the first differing element", + ), + ExpressionTestCase( + "prefix_shorter_first", + expression={"$lt": [[1], [1, 2]]}, + expected=True, + msg="Array ordering should place a prefix before the longer array", + ), + ExpressionTestCase( + "empty_before_nonempty", + expression={"$lt": [[], [1]]}, + expected=True, + msg="Array ordering should place the empty array before a non-empty array", + ), + ExpressionTestCase( + "numeric_equivalence", + expression={"$eq": [[1], [Int64(1)]]}, + expected=True, + msg="Array equality should apply numeric equivalence to elements", + ), + ExpressionTestCase( + "bool_vs_number_distinct", + expression={"$eq": [[True], [1]]}, + expected=False, + msg="Array equality should not coerce a bool element to a number", + ), + ExpressionTestCase( + "nested_recursion", + expression={"$lt": [[[1]], [[2]]]}, + expected=True, + msg="Array ordering should recurse into nested arrays", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ARRAY_COMPARISON_TESTS)) +def test_array_comparison(collection, test): + """Test array BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [Array Round-Trip Fidelity]: array values survive insert and +# retrieval unchanged. +ARRAY_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "empty_array", + value=[], + expected=[], + msg="Empty array should survive round-trip", + ), + RoundTripTestCase( + "nested_array", + value=[[1, 2], [3, [4, 5]]], + expected=[[1, 2], [3, [4, 5]]], + msg="Nested array should survive round-trip", + ), + RoundTripTestCase( + "mixed_types", + value=[1, "two", True, None, [3]], + expected=[1, "two", True, None, [3]], + msg="Array with mixed types should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ARRAY_ROUND_TRIP_TESTS)) +def test_array_round_trip(collection, test): + """Test array values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_binary.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_binary.py new file mode 100644 index 000000000..954497673 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_binary.py @@ -0,0 +1,132 @@ +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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [Binary Ordering]: Binary values order first by data length, then by +# data bytes, then by subtype, with subtypes ordered 0 < 1 < 3 < 4 < 5 < 128 +# < 2. +BINARY_ORDERING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty_equality", + expression={"$eq": [Binary(b"", 0), Binary(b"", 0)]}, + expected=True, + msg="Binary equality should hold for two empty binaries of the same subtype", + ), + ExpressionTestCase( + "empty_lt_nonempty", + expression={"$lt": [Binary(b"", 0), Binary(b"\x00", 0)]}, + expected=True, + msg="Binary ordering should place empty binary before any non-empty binary", + ), + ExpressionTestCase( + "length_before_bytes", + expression={"$lt": [Binary(b"\xff", 0), Binary(b"\x00\x00", 0)]}, + expected=True, + msg="Binary ordering should compare data length before data bytes", + ), + ExpressionTestCase( + "bytes_when_same_length", + expression={"$lt": [Binary(b"\x00", 0), Binary(b"\xff", 0)]}, + expected=True, + msg="Binary ordering should compare data bytes when length is equal", + ), + ExpressionTestCase( + "subtype_0_before_1", + expression={"$lt": [Binary(b"\x01" * 16, 0), Binary(b"\x01" * 16, 1)]}, + expected=True, + msg="Binary ordering should place subtype 0 before subtype 1", + ), + ExpressionTestCase( + "subtype_1_before_3", + expression={"$lt": [Binary(b"\x01" * 16, 1), Binary(b"\x01" * 16, 3)]}, + expected=True, + msg="Binary ordering should place subtype 1 before subtype 3", + ), + ExpressionTestCase( + "subtype_3_before_4", + expression={"$lt": [Binary(b"\x01" * 16, 3), Binary(b"\x01" * 16, 4)]}, + expected=True, + msg="Binary ordering should place subtype 3 before subtype 4", + ), + ExpressionTestCase( + "subtype_4_before_5", + expression={"$lt": [Binary(b"\x01" * 16, 4), Binary(b"\x01" * 16, 5)]}, + expected=True, + msg="Binary ordering should place subtype 4 before subtype 5", + ), + ExpressionTestCase( + "subtype_5_before_128", + expression={"$lt": [Binary(b"\x01" * 16, 5), Binary(b"\x01" * 16, 128)]}, + expected=True, + msg="Binary ordering should place subtype 5 before subtype 128", + ), + ExpressionTestCase( + "subtype_128_before_2", + expression={"$lt": [Binary(b"\x01" * 16, 128), Binary(b"\x01" * 16, 2)]}, + expected=True, + msg="Binary ordering should place subtype 128 before subtype 2", + ), + ExpressionTestCase( + "same_data_different_subtype_not_equal", + expression={"$eq": [Binary(b"hello", 0), Binary(b"hello", 5)]}, + expected=False, + msg="Binary equality should distinguish identical data with different subtypes", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(BINARY_ORDERING_TESTS)) +def test_binary_comparison(collection, test): + """Test Binary BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [Binary Round-Trip Fidelity]: Binary values survive insert and +# retrieval unchanged, preserving both data bytes and subtype. +BINARY_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "empty_subtype_0", + value=Binary(b"", 0), + expected=b"", + msg="Empty binary with subtype 0 should survive round-trip", + ), + RoundTripTestCase( + "subtype_0_data", + value=Binary(b"\x00\x01\x02\xff", 0), + expected=b"\x00\x01\x02\xff", + msg="Binary subtype 0 with data should survive round-trip", + ), + RoundTripTestCase( + "subtype_4_uuid", + value=Binary(b"\x01" * 16, 4), + expected=Binary(b"\x01" * 16, 4), + msg="Binary subtype 4 (UUID) should survive round-trip", + ), + RoundTripTestCase( + "subtype_128_user_defined", + value=Binary(b"custom", 128), + expected=Binary(b"custom", 128), + msg="Binary subtype 128 (user-defined) should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(BINARY_ROUND_TRIP_TESTS)) +def test_binary_round_trip(collection, test): + """Test Binary values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_bool.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_bool.py new file mode 100644 index 000000000..a99b1993c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_bool.py @@ -0,0 +1,64 @@ +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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [Bool Ordering]: false sorts before true, and true is not equal to +# false. +BOOL_COMPARISON_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "false_lt_true", + expression={"$lt": [False, True]}, + expected=True, + msg="Bool ordering should place false before true", + ), + ExpressionTestCase( + "true_ne_false", + expression={"$eq": [True, False]}, + expected=False, + msg="Bool equality should distinguish true from false", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(BOOL_COMPARISON_TESTS)) +def test_bool_comparison(collection, test): + """Test bool BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [Bool Round-Trip Fidelity]: bool values survive insert and retrieval +# unchanged. +BOOL_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "true_value", + value=True, + expected=True, + msg="True should survive round-trip", + ), + RoundTripTestCase( + "false_value", + value=False, + expected=False, + msg="False should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(BOOL_ROUND_TRIP_TESTS)) +def test_bool_round_trip(collection, test): + """Test bool values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_code.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_code.py new file mode 100644 index 000000000..aea9ec6ac --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_code.py @@ -0,0 +1,95 @@ +import pytest +from bson import Code + +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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [Code Ordering]: Code values compare by their code string +# lexicographically, and Code without scope is distinct from Code with scope. +CODE_COMPARISON_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "lexicographic_order", + expression={"$lt": [Code("function a() {}"), Code("function b() {}")]}, + expected=True, + msg="Code ordering should compare code strings lexicographically", + ), + ExpressionTestCase( + "same_code_equal", + expression={"$eq": [Code("function a() {}"), Code("function a() {}")]}, + expected=True, + msg="Code equality should hold for identical code strings", + ), + ExpressionTestCase( + "different_code_not_equal", + expression={"$eq": [Code("function a() {}"), Code("function b() {}")]}, + expected=False, + msg="Code equality should distinguish different code strings", + ), + ExpressionTestCase( + "empty_code_equal", + expression={"$eq": [Code(""), Code("")]}, + expected=True, + msg="Code equality should hold for two empty code strings", + ), + ExpressionTestCase( + "code_without_scope_ne_code_with_scope", + expression={"$eq": [Code("x"), Code("x", {})]}, + expected=False, + msg="Code without scope should not equal Code with scope even if code matches", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(CODE_COMPARISON_TESTS)) +def test_code_comparison(collection, test): + """Test Code BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [Code Round-Trip Fidelity]: Code values survive insert and retrieval +# unchanged, including the deprecated Code with scope variant. +CODE_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "empty_code", + value=Code(""), + expected=Code(""), + msg="Empty Code should survive round-trip", + ), + RoundTripTestCase( + "simple_code", + value=Code("function() { return 1; }"), + expected=Code("function() { return 1; }"), + msg="Simple Code should survive round-trip", + ), + RoundTripTestCase( + "code_with_scope", + value=Code("function() { return x; }", {"x": 1}), + expected=Code("function() { return x; }", {"x": 1}), + msg="Code with scope should survive round-trip", + ), + RoundTripTestCase( + "code_with_empty_scope", + value=Code("return 1", {}), + expected=Code("return 1", {}), + msg="Code with empty scope should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(CODE_ROUND_TRIP_TESTS)) +def test_code_round_trip(collection, test): + """Test Code values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_date.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_date.py new file mode 100644 index 000000000..15630b215 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_date.py @@ -0,0 +1,153 @@ +from datetime import datetime, timedelta, timezone + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.expression_test_case import ( # noqa: E501 + ExpressionTestCase, +) +from documentdb_tests.compatibility.tests.core.operator.expressions.utils.utils import ( + assert_expression_result, + execute_expression, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [Date Signed Epoch Offset]: BSON Date stores time as a signed +# 64-bit millisecond offset from the Unix epoch. +DATE_ORDERING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "pre_epoch_before_post_epoch", + expression={ + "$lt": [ + datetime(1969, 12, 31, 23, 59, 59, tzinfo=timezone.utc), + datetime(1970, 1, 1, 0, 0, 1, tzinfo=timezone.utc), + ] + }, + expected=True, + msg="Pre-epoch date (negative offset) should sort before post-epoch date", + ), + ExpressionTestCase( + "min_before_max", + expression={ + "$lt": [ + datetime(1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + datetime(9999, 12, 31, 23, 59, 59, 999000, tzinfo=timezone.utc), + ] + }, + expected=True, + msg="Minimum representable date should sort before maximum representable date", + ), +] + +# Property [Date Millisecond Precision]: BSON Date has millisecond granularity; +# sub-millisecond differences are truncated. +DATE_PRECISION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "sub_millisecond_truncated_equal", + # Python datetime carries microseconds but BSON Date stores + # milliseconds, so 0us and 999us within the same ms compare equal. + expression={ + "$eq": [ + datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc), + datetime(2024, 1, 1, 0, 0, 0, 999, tzinfo=timezone.utc), + ] + }, + expected=True, + msg="Sub-millisecond differences should be truncated to the same ms", + ), + ExpressionTestCase( + "crossing_millisecond_boundary_not_equal", + # 999us rounds to ms 0, 1000us rounds to ms 1 - distinct values. + expression={ + "$eq": [ + datetime(2024, 1, 1, 0, 0, 0, 999, tzinfo=timezone.utc), + datetime(2024, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc), + ] + }, + expected=False, + msg="Crossing a millisecond boundary should produce distinct date values", + ), +] + +# Property [Date Timezone Normalization]: BSON Date stores only UTC +# milliseconds; timezone offset is normalized away. +DATE_TIMEZONE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "same_instant_different_tz_equal", + expression={ + "$eq": [ + datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone(timedelta(hours=-5))), + datetime(2024, 6, 15, 17, 0, 0, tzinfo=timezone.utc), + ] + }, + expected=True, + msg="Same instant in different timezones should compare equal", + ), + ExpressionTestCase( + "same_wall_clock_different_tz_not_equal", + expression={ + "$eq": [ + datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone(timedelta(hours=-5))), + datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), + ] + }, + expected=False, + msg="Same wall-clock time in different timezones should not compare equal", + ), +] + +DATE_COMPARISON_TESTS = DATE_ORDERING_TESTS + DATE_PRECISION_TESTS + DATE_TIMEZONE_TESTS + + +@pytest.mark.parametrize("test", pytest_params(DATE_COMPARISON_TESTS)) +def test_date_comparison(collection, test): + """Test Date BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [Date Round-Trip Fidelity]: Date values survive insert and retrieval +# unchanged across the full representable range. +DATE_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "min_date", + value=datetime(1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + expected=datetime(1, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="Minimum representable date should survive round-trip", + ), + RoundTripTestCase( + "pre_epoch", + value=datetime(1969, 12, 31, 23, 59, 59, tzinfo=timezone.utc), + expected=datetime(1969, 12, 31, 23, 59, 59, tzinfo=timezone.utc), + msg="Pre-epoch date should survive round-trip", + ), + RoundTripTestCase( + "epoch", + value=datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + expected=datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc), + msg="Epoch date should survive round-trip", + ), + RoundTripTestCase( + "max_date", + value=datetime(9999, 12, 31, 23, 59, 59, 999000, tzinfo=timezone.utc), + expected=datetime(9999, 12, 31, 23, 59, 59, 999000, tzinfo=timezone.utc), + msg="Maximum representable date should survive round-trip", + ), + RoundTripTestCase( + "non_utc_normalized_to_utc", + value=datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone(timedelta(hours=-5))), + expected=datetime(2024, 6, 15, 17, 0, 0, tzinfo=timezone.utc), + msg="Non-UTC date should be stored as equivalent UTC instant", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(DATE_ROUND_TRIP_TESTS)) +def test_date_round_trip(collection, test): + """Test Date values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_minkey_maxkey.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_minkey_maxkey.py new file mode 100644 index 000000000..669a6960b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_minkey_maxkey.py @@ -0,0 +1,77 @@ +import pytest +from bson import MaxKey, MinKey + +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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [MinKey/MaxKey Distinction]: MinKey and MaxKey are distinct +# singleton types, each equal only to itself, with MinKey sorting before MaxKey. +MINKEY_MAXKEY_COMPARISON_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "minkey_eq_minkey", + expression={"$eq": [MinKey(), MinKey()]}, + expected=True, + msg="MinKey should compare equal to MinKey", + ), + ExpressionTestCase( + "maxkey_eq_maxkey", + expression={"$eq": [MaxKey(), MaxKey()]}, + expected=True, + msg="MaxKey should compare equal to MaxKey", + ), + ExpressionTestCase( + "minkey_ne_maxkey", + expression={"$eq": [MinKey(), MaxKey()]}, + expected=False, + msg="MinKey should not compare equal to MaxKey", + ), + ExpressionTestCase( + "minkey_lt_maxkey", + expression={"$lt": [MinKey(), MaxKey()]}, + expected=True, + msg="MinKey should sort before MaxKey", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(MINKEY_MAXKEY_COMPARISON_TESTS)) +def test_minkey_maxkey_comparison(collection, test): + """Test MinKey/MaxKey BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [MinKey/MaxKey Round-Trip Fidelity]: MinKey and MaxKey values survive +# insert and retrieval unchanged. +MINKEY_MAXKEY_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "minkey", + value=MinKey(), + expected=MinKey(), + msg="MinKey should survive round-trip", + ), + RoundTripTestCase( + "maxkey", + value=MaxKey(), + expected=MaxKey(), + msg="MaxKey should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(MINKEY_MAXKEY_ROUND_TRIP_TESTS)) +def test_minkey_maxkey_round_trip(collection, test): + """Test MinKey/MaxKey values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_null.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_null.py new file mode 100644 index 000000000..ba66101ac --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_null.py @@ -0,0 +1,69 @@ +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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [Null Equality]: null is equal to null but distinct from other types. +NULL_COMPARISON_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "null_eq_null", + expression={"$eq": [None, None]}, + expected=True, + msg="Null should equal null", + ), + ExpressionTestCase( + "null_ne_zero", + expression={"$eq": [None, 0]}, + expected=False, + msg="Null should not equal zero", + ), + ExpressionTestCase( + "null_ne_empty_string", + expression={"$eq": [None, ""]}, + expected=False, + msg="Null should not equal empty string", + ), + ExpressionTestCase( + "null_ne_false", + expression={"$eq": [None, False]}, + expected=False, + msg="Null should not equal false", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(NULL_COMPARISON_TESTS)) +def test_null_comparison(collection, test): + """Test null BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [Null Round-Trip Fidelity]: an explicit null field value survives +# insert and retrieval unchanged. +NULL_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "explicit_null", + value=None, + expected=None, + msg="Explicit null field should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(NULL_ROUND_TRIP_TESTS)) +def test_null_round_trip(collection, test): + """Test null values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_numeric.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_numeric.py new file mode 100644 index 000000000..9788a65bc --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_numeric.py @@ -0,0 +1,503 @@ +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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_MAX, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_NAN, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_ZERO, + DOUBLE_FROM_INT64_MAX, + DOUBLE_MAX, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_MIN_SUBNORMAL, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_PRECISION_LOSS, + DOUBLE_ZERO, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + FLOAT_NEGATIVE_NAN, + INT32_MAX, + INT32_MIN, + INT32_OVERFLOW, + INT64_MAX, + INT64_MIN, + INT64_ZERO, +) + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [Numeric Type Equivalence]: numeric values with the same +# mathematical value are equal regardless of whether they are int32, Int64, +# double, or Decimal128. +NUMERIC_EQUIVALENCE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int_long", + expression={"$eq": [1, Int64(1)]}, + expected=True, + msg="Numeric equivalence should hold for int32 and Int64", + ), + ExpressionTestCase( + "int_double", + expression={"$eq": [1, 1.0]}, + expected=True, + msg="Numeric equivalence should hold for int32 and double", + ), + ExpressionTestCase( + "int_decimal", + expression={"$eq": [1, Decimal128("1")]}, + expected=True, + msg="Numeric equivalence should hold for int32 and Decimal128", + ), + ExpressionTestCase( + "long_double", + expression={"$eq": [Int64(1), 1.0]}, + expected=True, + msg="Numeric equivalence should hold for Int64 and double", + ), + ExpressionTestCase( + "long_decimal", + expression={"$eq": [Int64(1), Decimal128("1")]}, + expected=True, + msg="Numeric equivalence should hold for Int64 and Decimal128", + ), + ExpressionTestCase( + "double_decimal", + expression={"$eq": [1.0, Decimal128("1")]}, + expected=True, + msg="Numeric equivalence should hold for double and Decimal128", + ), + ExpressionTestCase( + "negative_cross_type", + expression={"$eq": [-1, Int64(-1)]}, + expected=True, + msg="Numeric equivalence should hold for negative int32 and Int64", + ), + ExpressionTestCase( + "negative_int_double", + expression={"$eq": [-1, -1.0]}, + expected=True, + msg="Numeric equivalence should hold for negative int32 and double", + ), + ExpressionTestCase( + "negative_int_decimal", + expression={"$eq": [-1, Decimal128("-1")]}, + expected=True, + msg="Numeric equivalence should hold for negative int32 and Decimal128", + ), +] + +# Property [Numeric Boundary Equivalence]: cross-type equivalence holds at type +# boundaries where exact representation is possible. +NUMERIC_BOUNDARY_EQUIVALENCE_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_max_as_double", + expression={"$eq": [INT32_MAX, float(INT32_MAX)]}, + expected=True, + msg="Numeric equivalence should hold for int32 max and its exact double", + ), + ExpressionTestCase( + "int32_max_as_long", + expression={"$eq": [INT32_MAX, Int64(INT32_MAX)]}, + expected=True, + msg="Numeric equivalence should hold for int32 max and the same Int64 value", + ), + ExpressionTestCase( + "int32_min_as_long", + expression={"$eq": [INT32_MIN, Int64(INT32_MIN)]}, + expected=True, + msg="Numeric equivalence should hold for int32 min and the same Int64 value", + ), + ExpressionTestCase( + "int32_min_as_double", + expression={"$eq": [INT32_MIN, float(INT32_MIN)]}, + expected=True, + msg="Numeric equivalence should hold for int32 min and its exact double", + ), + ExpressionTestCase( + "int64_max_as_decimal", + expression={"$eq": [INT64_MAX, Decimal128(str(INT64_MAX))]}, + expected=True, + msg="Numeric equivalence should hold for Int64 max and its exact Decimal128", + ), + ExpressionTestCase( + "int64_min_as_decimal", + expression={"$eq": [INT64_MIN, Decimal128(str(INT64_MIN))]}, + expected=True, + msg="Numeric equivalence should hold for Int64 min and its exact Decimal128", + ), +] + +# Property [Infinity Equivalence and Ordering]: positive and negative infinity +# are equal across double and Decimal128, distinct from each other, and bound +# all finite values. +INFINITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "positive_infinity_cross_type", + expression={"$eq": [FLOAT_INFINITY, DECIMAL128_INFINITY]}, + expected=True, + msg="Double positive infinity should equal Decimal128 positive infinity", + ), + ExpressionTestCase( + "negative_infinity_cross_type", + expression={"$eq": [FLOAT_NEGATIVE_INFINITY, DECIMAL128_NEGATIVE_INFINITY]}, + expected=True, + msg="Double negative infinity should equal Decimal128 negative infinity", + ), + ExpressionTestCase( + "positive_ne_negative_infinity_double", + expression={"$eq": [FLOAT_INFINITY, FLOAT_NEGATIVE_INFINITY]}, + expected=False, + msg="Double positive infinity should not equal double negative infinity", + ), + ExpressionTestCase( + "positive_ne_negative_infinity_decimal", + expression={"$eq": [DECIMAL128_INFINITY, DECIMAL128_NEGATIVE_INFINITY]}, + expected=False, + msg="Decimal128 positive infinity should not equal Decimal128 negative infinity", + ), + ExpressionTestCase( + "max_double_lt_double_infinity", + expression={"$lt": [DOUBLE_MAX, FLOAT_INFINITY]}, + expected=True, + msg="The largest finite double should be less than double positive infinity", + ), + ExpressionTestCase( + "max_double_lt_decimal_infinity", + expression={"$lt": [DOUBLE_MAX, DECIMAL128_INFINITY]}, + expected=True, + msg="The largest finite double should be less than Decimal128 positive infinity", + ), + ExpressionTestCase( + "double_negative_infinity_lt_negative_max", + expression={"$lt": [FLOAT_NEGATIVE_INFINITY, -DOUBLE_MAX]}, + expected=True, + msg="Double negative infinity should be less than the most negative finite double", + ), + ExpressionTestCase( + "decimal_negative_infinity_lt_negative_max", + expression={"$lt": [DECIMAL128_NEGATIVE_INFINITY, -DOUBLE_MAX]}, + expected=True, + msg="Decimal128 negative infinity should be less than the most negative finite double", + ), +] + +# Property [Negative Zero Equivalence]: negative zero equals positive zero +# across all numeric types. +NEGATIVE_ZERO_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "double_neg_zero", + expression={"$eq": [DOUBLE_NEGATIVE_ZERO, DOUBLE_ZERO]}, + expected=True, + msg="Negative zero should equal positive zero for double", + ), + ExpressionTestCase( + "decimal_neg_zero", + expression={"$eq": [DECIMAL128_NEGATIVE_ZERO, DECIMAL128_ZERO]}, + expected=True, + msg="Negative zero should equal positive zero for Decimal128", + ), + ExpressionTestCase( + "neg_zero_int", + expression={"$eq": [DOUBLE_NEGATIVE_ZERO, 0]}, + expected=True, + msg="Negative zero should equal int32 zero", + ), + ExpressionTestCase( + "neg_zero_long", + expression={"$eq": [DOUBLE_NEGATIVE_ZERO, INT64_ZERO]}, + expected=True, + msg="Negative zero should equal Int64 zero", + ), + ExpressionTestCase( + "neg_zero_cross_decimal", + expression={"$eq": [DOUBLE_NEGATIVE_ZERO, DECIMAL128_ZERO]}, + expected=True, + msg="Double negative zero should equal Decimal128 zero", + ), +] + +# Property [NaN Equality and Ordering]: NaN equals NaN (including negative and +# cross-type), is never equal to a non-NaN value, and sorts below all other +# numeric values. +NAN_EQUALITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "nan_nan", + expression={"$eq": [FLOAT_NAN, FLOAT_NAN]}, + expected=True, + msg="BSON equality should treat NaN as equal to NaN", + ), + ExpressionTestCase( + "nan_negative_nan", + expression={"$eq": [FLOAT_NAN, FLOAT_NEGATIVE_NAN]}, + expected=True, + msg="BSON equality should treat NaN as equal to negative NaN", + ), + ExpressionTestCase( + "decimal_nan_decimal_nan", + expression={"$eq": [DECIMAL128_NAN, DECIMAL128_NAN]}, + expected=True, + msg="BSON equality should treat Decimal128 NaN as equal to Decimal128 NaN", + ), + ExpressionTestCase( + "decimal_nan_negative_nan", + expression={"$eq": [DECIMAL128_NAN, DECIMAL128_NEGATIVE_NAN]}, + expected=True, + msg="BSON equality should treat Decimal128 NaN as equal to Decimal128 negative NaN", + ), + ExpressionTestCase( + "nan_cross_type", + expression={"$eq": [FLOAT_NAN, DECIMAL128_NAN]}, + expected=True, + msg="BSON equality should treat double NaN as equal to Decimal128 NaN", + ), + ExpressionTestCase( + "nan_ne_int", + expression={"$eq": [FLOAT_NAN, 1]}, + expected=False, + msg="NaN should not equal a number", + ), + ExpressionTestCase( + "nan_ne_null", + expression={"$eq": [FLOAT_NAN, None]}, + expected=False, + msg="NaN should not equal null", + ), + ExpressionTestCase( + "double_nan_lt_negative_infinity", + expression={"$lt": [FLOAT_NAN, FLOAT_NEGATIVE_INFINITY]}, + expected=True, + msg="Double NaN should sort below negative infinity", + ), + ExpressionTestCase( + "decimal_nan_lt_negative_infinity", + expression={"$lt": [DECIMAL128_NAN, DECIMAL128_NEGATIVE_INFINITY]}, + expected=True, + msg="Decimal128 NaN should sort below negative infinity", + ), + ExpressionTestCase( + "double_nan_lt_zero", + expression={"$lt": [FLOAT_NAN, 0]}, + expected=True, + msg="Double NaN should sort below zero", + ), + ExpressionTestCase( + "decimal_nan_lt_zero", + expression={"$lt": [DECIMAL128_NAN, 0]}, + expected=True, + msg="Decimal128 NaN should sort below zero", + ), +] + +# Property [Decimal128 Precision]: Decimal128 values that represent the same +# mathematical value are equal regardless of trailing zeros, scientific +# notation, or the exponent used to represent zero. +DECIMAL128_PRECISION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "trailing_zeros", + expression={"$eq": [Decimal128("2.0"), Decimal128("2.00")]}, + expected=True, + msg="Decimal128 equality should ignore trailing zeros", + ), + ExpressionTestCase( + "scientific_notation", + expression={"$eq": [Decimal128("3E+2"), Decimal128("300")]}, + expected=True, + msg="Decimal128 equality should treat scientific notation as equal to standard form", + ), + ExpressionTestCase( + "zero_different_exponents", + expression={"$eq": [Decimal128("0E-6176"), Decimal128("0E+0")]}, + expected=True, + msg="Decimal128 equality should treat zeros with different exponents as equal", + ), + ExpressionTestCase( + "zero_extreme_exponents", + expression={"$eq": [Decimal128("0E-6176"), Decimal128("0E+6111")]}, + expected=True, + msg="Decimal128 equality should treat zeros across the exponent range as equal", + ), +] + +# Property [Double Precision Boundary]: a double can only exactly represent +# integers up to 2^53, so an Int64 within that range equals its double while an +# Int64 just beyond it does not, and a double cannot exactly represent Int64 +# max. +DOUBLE_PRECISION_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int64_max_safe_eq_double", + expression={"$eq": [Int64(DOUBLE_MAX_SAFE_INTEGER), float(DOUBLE_MAX_SAFE_INTEGER)]}, + expected=True, + msg="An Int64 at the 2^53 boundary should equal its exact double representation", + ), + ExpressionTestCase( + "int64_precision_loss_ne_double", + expression={"$eq": [Int64(DOUBLE_PRECISION_LOSS), float(DOUBLE_PRECISION_LOSS)]}, + expected=False, + msg="An Int64 just beyond 2^53 should not equal its rounded double representation", + ), + ExpressionTestCase( + "int64_max_ne_double", + expression={"$eq": [INT64_MAX, DOUBLE_FROM_INT64_MAX]}, + expected=False, + msg="Int64 max should not equal the double that cannot exactly represent it", + ), + ExpressionTestCase( + "zero_lt_smallest_subnormal", + expression={"$lt": [DOUBLE_ZERO, DOUBLE_MIN_SUBNORMAL]}, + expected=True, + msg="Zero should be less than the smallest positive subnormal double", + ), + ExpressionTestCase( + "double_tenth_ne_decimal_tenth", + expression={"$eq": [0.1, Decimal128("0.1")]}, + expected=False, + msg="Double 0.1 should not equal Decimal128 0.1 because of IEEE 754 representation", + ), + ExpressionTestCase( + "decimal_tenth_lt_double_tenth", + expression={"$lt": [Decimal128("0.1"), 0.1]}, + expected=True, + msg="Decimal128 0.1 should be less than double 0.1 because the double rounds up", + ), +] + +# Property [Cross-Type Numeric Ordering]: numeric values of different subtypes +# order by mathematical value. +CROSS_TYPE_ORDERING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "int32_max_lt_next_int64", + expression={"$lt": [INT32_MAX, Int64(INT32_OVERFLOW)]}, + expected=True, + msg="Cross-type ordering should place int32 max below the next Int64 value", + ), + ExpressionTestCase( + "int64_min_lt_int32_min", + expression={"$lt": [INT64_MIN, INT32_MIN]}, + expected=True, + msg="Cross-type ordering should place Int64 min below int32 min", + ), + ExpressionTestCase( + "int64_max_lt_decimal128_max", + expression={"$lt": [INT64_MAX, DECIMAL128_MAX]}, + expected=True, + msg="Cross-type ordering should place Int64 max below Decimal128 max", + ), +] + +NUMERIC_COMPARISON_TESTS = ( + NUMERIC_EQUIVALENCE_TESTS + + NUMERIC_BOUNDARY_EQUIVALENCE_TESTS + + INFINITY_TESTS + + NEGATIVE_ZERO_TESTS + + NAN_EQUALITY_TESTS + + DECIMAL128_PRECISION_TESTS + + DOUBLE_PRECISION_TESTS + + CROSS_TYPE_ORDERING_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(NUMERIC_COMPARISON_TESTS)) +def test_numeric_comparison(collection, test): + """Test numeric BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [Numeric Round-Trip Fidelity]: numeric values at type boundaries +# survive insert and retrieval unchanged. +NUMERIC_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "int32_max", + value=INT32_MAX, + expected=INT32_MAX, + msg="Int32 max should survive round-trip", + ), + RoundTripTestCase( + "int32_min", + value=INT32_MIN, + expected=INT32_MIN, + msg="Int32 min should survive round-trip", + ), + RoundTripTestCase( + "int64_max", + value=INT64_MAX, + expected=INT64_MAX, + msg="Int64 max should survive round-trip", + ), + RoundTripTestCase( + "int64_min", + value=INT64_MIN, + expected=INT64_MIN, + msg="Int64 min should survive round-trip", + ), + RoundTripTestCase( + "double_max", + value=DOUBLE_MAX, + expected=DOUBLE_MAX, + msg="Double max should survive round-trip", + ), + RoundTripTestCase( + "double_min_subnormal", + value=DOUBLE_MIN_SUBNORMAL, + expected=DOUBLE_MIN_SUBNORMAL, + msg="Smallest subnormal double should survive round-trip", + ), + RoundTripTestCase( + "double_infinity", + value=FLOAT_INFINITY, + expected=FLOAT_INFINITY, + msg="Double positive infinity should survive round-trip", + ), + RoundTripTestCase( + "double_negative_infinity", + value=FLOAT_NEGATIVE_INFINITY, + expected=FLOAT_NEGATIVE_INFINITY, + msg="Double negative infinity should survive round-trip", + ), + RoundTripTestCase( + "decimal128_max", + value=DECIMAL128_MAX, + expected=DECIMAL128_MAX, + msg="Decimal128 max should survive round-trip", + ), + RoundTripTestCase( + "decimal128_nan", + value=DECIMAL128_NAN, + expected=DECIMAL128_NAN, + msg="Decimal128 NaN should survive round-trip", + ), + RoundTripTestCase( + "decimal128_infinity", + value=DECIMAL128_INFINITY, + expected=DECIMAL128_INFINITY, + msg="Decimal128 positive infinity should survive round-trip", + ), + RoundTripTestCase( + "decimal128_negative_infinity", + value=DECIMAL128_NEGATIVE_INFINITY, + expected=DECIMAL128_NEGATIVE_INFINITY, + msg="Decimal128 negative infinity should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(NUMERIC_ROUND_TRIP_TESTS)) +def test_numeric_round_trip(collection, test): + """Test numeric values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_object.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_object.py new file mode 100644 index 000000000..b976678bc --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_object.py @@ -0,0 +1,108 @@ +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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [Document Ordering]: documents compare field by field in stored +# order, comparing the field name before the value within each pair, and a +# document that is a prefix of another sorts before the longer document. +OBJECT_COMPARISON_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "empty_equality", + expression={"$eq": [{}, {}]}, + expected=True, + msg="Document equality should hold for two empty documents", + ), + ExpressionTestCase( + "value_when_keys_equal", + expression={"$lt": [{"a": 1, "b": 1}, {"a": 1, "b": 2}]}, + expected=True, + msg="Document ordering should compare values when field names are equal", + ), + ExpressionTestCase( + "key_before_value", + expression={"$lt": [{"a": 2}, {"b": 1}]}, + expected=True, + msg="Document ordering should compare field names before values within each pair", + ), + ExpressionTestCase( + "key_diff_value_equal", + expression={"$lt": [{"a": 1}, {"b": 1}]}, + expected=True, + msg="Document ordering should compare field names when values are equal", + ), + ExpressionTestCase( + "prefix_shorter_first", + expression={"$lt": [{"a": 1}, {"a": 1, "b": 1}]}, + expected=True, + msg="Document ordering should place a prefix document before a longer document", + ), + ExpressionTestCase( + "empty_before_nonempty", + expression={"$lt": [{}, {"a": 1}]}, + expected=True, + msg="Document ordering should place the empty document before a non-empty document", + ), + ExpressionTestCase( + "nested_value_recurse", + expression={"$lt": [{"a": {"b": 1}}, {"a": {"b": 2}}]}, + expected=True, + msg="Document ordering should recurse into nested document values", + ), + ExpressionTestCase( + "numeric_equivalence_in_value", + expression={"$eq": [{"x": 1}, {"x": Int64(1)}]}, + expected=True, + msg="Document equality should apply numeric equivalence to field values", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(OBJECT_COMPARISON_TESTS)) +def test_object_comparison(collection, test): + """Test embedded document BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [Document Round-Trip Fidelity]: embedded documents survive insert +# and retrieval unchanged, preserving field order. +OBJECT_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "empty_doc", + value={}, + expected={}, + msg="Empty document should survive round-trip", + ), + RoundTripTestCase( + "nested_doc", + value={"a": {"b": {"c": 1}}}, + expected={"a": {"b": {"c": 1}}}, + msg="Nested document should survive round-trip", + ), + RoundTripTestCase( + "mixed_value_types", + value={"s": "hello", "n": 42, "b": True, "null": None, "arr": [1, 2]}, + expected={"s": "hello", "n": 42, "b": True, "null": None, "arr": [1, 2]}, + msg="Document with mixed value types should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(OBJECT_ROUND_TRIP_TESTS)) +def test_object_round_trip(collection, test): + """Test embedded document values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_objectid.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_objectid.py new file mode 100644 index 000000000..9809570fb --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_objectid.py @@ -0,0 +1,122 @@ +import pytest +from bson import ObjectId + +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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [ObjectId Equality]: ObjectIds are equal only when all 12 bytes +# match. +OBJECTID_EQUALITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "exact_match_equal", + expression={ + "$eq": [ObjectId("507f1f77bcf86cd799439011"), ObjectId("507f1f77bcf86cd799439011")] + }, + expected=True, + msg="ObjectId equality should hold for identical 12-byte values", + ), + ExpressionTestCase( + "last_byte_differs_not_equal", + expression={ + "$eq": [ObjectId("507f1f77bcf86cd799439011"), ObjectId("507f1f77bcf86cd799439012")] + }, + expected=False, + msg="ObjectId equality should distinguish values differing in the last byte", + ), +] + +# Property [ObjectId Byte Ordering]: ObjectIds order by lexicographic byte +# comparison. +OBJECTID_BYTE_ORDERING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "leading_byte_dominates", + expression={ + "$lt": [ObjectId("000000000000000000000001"), ObjectId("ffffffffffffffffffff0001")] + }, + expected=True, + msg="ObjectId ordering should let leading bytes dominate trailing bytes", + ), + ExpressionTestCase( + "trailing_byte_tiebreak", + expression={ + "$lt": [ObjectId("0000000000000000000000aa"), ObjectId("0000000000000000000000bb")] + }, + expected=True, + msg="ObjectId ordering should compare trailing bytes when leading bytes are equal", + ), + ExpressionTestCase( + "min_oid_lt_max_oid", + expression={"$lt": [ObjectId("0" * 24), ObjectId("f" * 24)]}, + expected=True, + msg="ObjectId ordering should place the all-zeros minimum before the all-0xff maximum", + ), +] + +# Property [ObjectId Timestamp Component]: the leading 4-byte timestamp +# component orders ObjectIds chronologically. +OBJECTID_TIMESTAMP_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "earlier_timestamp_first", + expression={ + "$lt": [ + ObjectId("5e0be100" + "0" * 16), + ObjectId("65920080" + "0" * 16), + ] + }, + expected=True, + msg="ObjectId ordering should place an earlier timestamp prefix before a later one", + ), +] + +OBJECTID_COMPARISON_TESTS = ( + OBJECTID_EQUALITY_TESTS + OBJECTID_BYTE_ORDERING_TESTS + OBJECTID_TIMESTAMP_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(OBJECTID_COMPARISON_TESTS)) +def test_objectid_comparison(collection, test): + """Test ObjectId BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [ObjectId Round-Trip Fidelity]: ObjectId values survive insert and +# retrieval unchanged. +OBJECTID_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "min_oid", + value=ObjectId("0" * 24), + expected=ObjectId("0" * 24), + msg="All-zeros ObjectId should survive round-trip", + ), + RoundTripTestCase( + "max_oid", + value=ObjectId("f" * 24), + expected=ObjectId("f" * 24), + msg="All-0xff ObjectId should survive round-trip", + ), + RoundTripTestCase( + "typical_oid", + value=ObjectId("507f1f77bcf86cd799439011"), + expected=ObjectId("507f1f77bcf86cd799439011"), + msg="Typical ObjectId should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(OBJECTID_ROUND_TRIP_TESTS)) +def test_objectid_round_trip(collection, test): + """Test ObjectId values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_regex.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_regex.py new file mode 100644 index 000000000..6ab0eab44 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_regex.py @@ -0,0 +1,89 @@ +import pytest +from bson import Regex + +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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [Regex Ordering]: regex values compare by pattern first, then by +# flags, and flag order is normalized so the same flags in any order are equal. +REGEX_COMPARISON_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "pattern_first", + expression={"$lt": [Regex("abc", ""), Regex("abd", "")]}, + expected=True, + msg="Regex ordering should compare pattern before flags", + ), + ExpressionTestCase( + "flags_after_pattern", + expression={"$lt": [Regex("abc", ""), Regex("abc", "i")]}, + expected=True, + msg="Regex ordering should compare flags when patterns are equal", + ), + ExpressionTestCase( + "flag_order_normalized", + expression={"$eq": [Regex("abc", "im"), Regex("abc", "mi")]}, + expected=True, + msg="Regex equality should normalize flag order", + ), + ExpressionTestCase( + "different_flags_distinct", + expression={"$eq": [Regex("abc", "i"), Regex("abc", "m")]}, + expected=False, + msg="Regex equality should distinguish the same pattern with different flags", + ), + ExpressionTestCase( + "same_pattern_and_flags_equal", + expression={"$eq": [Regex("^test$", "i"), Regex("^test$", "i")]}, + expected=True, + msg="Regex equality should hold for identical pattern and flags", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(REGEX_COMPARISON_TESTS)) +def test_regex_comparison(collection, test): + """Test Regex BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [Regex Round-Trip Fidelity]: Regex values survive insert and +# retrieval unchanged, preserving pattern and flags. +REGEX_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "simple_pattern", + value=Regex("^hello$", ""), + expected=Regex("^hello$", ""), + msg="Simple regex pattern should survive round-trip", + ), + RoundTripTestCase( + "pattern_with_flags", + value=Regex("test", "ims"), + expected=Regex("test", "ims"), + msg="Regex with flags should survive round-trip", + ), + RoundTripTestCase( + "empty_pattern", + value=Regex("", ""), + expected=Regex("", ""), + msg="Empty regex pattern should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(REGEX_ROUND_TRIP_TESTS)) +def test_regex_round_trip(collection, test): + """Test Regex values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_string.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_string.py new file mode 100644 index 000000000..a289cf867 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_string.py @@ -0,0 +1,181 @@ +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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import STRING_SIZE_LIMIT_BYTES + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [String Equality]: string equality is byte-exact with no Unicode +# normalization. +STRING_EQUALITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "str_equal", + expression={"$eq": ["abc", "abc"]}, + expected=True, + msg="String equality should hold for identical strings", + ), + ExpressionTestCase( + "str_empty_equal", + expression={"$eq": ["", ""]}, + expected=True, + msg="String equality should hold for two empty strings", + ), + ExpressionTestCase( + "str_case_sensitive", + expression={"$eq": ["abc", "ABC"]}, + expected=False, + msg="String equality should be case-sensitive", + ), + ExpressionTestCase( + "str_different", + expression={"$eq": ["abc", "abd"]}, + expected=False, + msg="String equality should distinguish different strings", + ), + ExpressionTestCase( + "str_different_length", + expression={"$eq": ["abc", "abcd"]}, + expected=False, + msg="String equality should distinguish strings of different length", + ), + ExpressionTestCase( + "str_null_byte_significant", + expression={"$eq": ["abc\0", "abc"]}, + expected=False, + msg="String equality should treat a trailing null byte as significant", + ), + ExpressionTestCase( + # U+00E9 precomposed vs "e" + U+0301 combining acute accent. + "str_no_unicode_normalization", + expression={"$eq": ["caf\u00e9", "cafe\u0301"]}, + expected=False, + msg="String equality should not apply Unicode normalization", + ), + ExpressionTestCase( + "str_emoji_equal", + expression={"$eq": ["\U0001f600", "\U0001f600"]}, + expected=True, + msg="String equality should hold for identical emoji", + ), + ExpressionTestCase( + "str_cjk_equal", + expression={"$eq": ["\u4e16\u754c", "\u4e16\u754c"]}, + expected=True, + msg="String equality should hold for identical CJK strings", + ), + ExpressionTestCase( + "str_cjk_different", + expression={"$eq": ["\u4e16\u754c", "\u4e16\u754d"]}, + expected=False, + msg="String equality should distinguish different CJK strings", + ), +] + +# Property [String Ordering]: strings order by lexicographic byte comparison. +STRING_ORDERING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "str_lt_lexicographic", + expression={"$lt": ["abc", "abd"]}, + expected=True, + msg="String ordering should compare strings byte by byte", + ), + ExpressionTestCase( + "str_prefix_shorter_first", + expression={"$lt": ["a", "aa"]}, + expected=True, + msg="String ordering should place a prefix before the longer string", + ), + ExpressionTestCase( + "str_empty_before_nonempty", + expression={"$lt": ["", "a"]}, + expected=True, + msg="String ordering should place the empty string before a non-empty string", + ), + ExpressionTestCase( + "str_uppercase_before_lowercase", + # 'A' is U+0041, 'a' is U+0061, so byte ordering puts uppercase first. + expression={"$lt": ["A", "a"]}, + expected=True, + msg="String ordering should place uppercase before lowercase by byte value", + ), + ExpressionTestCase( + "str_null_byte_after_prefix", + # 'a' is a prefix of 'a\0', so the prefix sorts first. + expression={"$lt": ["a", "a\0"]}, + expected=True, + msg="String ordering should treat a trailing null byte as extending the string", + ), +] + +STRING_COMPARISON_TESTS = STRING_EQUALITY_TESTS + STRING_ORDERING_TESTS + + +@pytest.mark.parametrize("test", pytest_params(STRING_COMPARISON_TESTS)) +def test_string_comparison(collection, test): + """Test string BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [String Round-Trip Fidelity]: string values survive insert and +# retrieval unchanged. +STRING_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "empty_string", + value="", + expected="", + msg="Empty string should survive round-trip", + ), + RoundTripTestCase( + "null_byte", + value="abc\x00def", + expected="abc\x00def", + msg="String with embedded null byte should survive round-trip", + ), + RoundTripTestCase( + "emoji", + value="\U0001f600", + expected="\U0001f600", + msg="Emoji string should survive round-trip", + ), + RoundTripTestCase( + "cjk", + value="\u4e16\u754c", + expected="\u4e16\u754c", + msg="CJK string should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(STRING_ROUND_TRIP_TESTS)) +def test_string_round_trip(collection, test): + """Test string values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) + + +def test_string_max_length(collection): + """Test max-length string passes through the engine unchanged.""" + max_str = "a" * (STRING_SIZE_LIMIT_BYTES - 1) + result = execute_command( + collection, + { + "aggregate": 1, + "pipeline": [ + {"$documents": [{"v": max_str}]}, + {"$project": {"_id": 0, "v": 1}}, + ], + "cursor": {}, + }, + ) + assertSuccess(result, [{"v": max_str}], msg="Max-length string should pass through unchanged") diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_timestamp.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_timestamp.py new file mode 100644 index 000000000..3da975e0d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/types/test_types_timestamp.py @@ -0,0 +1,135 @@ +import pytest +from bson import 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.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +from ..utils.round_trip_test_case import RoundTripTestCase + +# Property [Timestamp Ordering]: Timestamps order by their seconds component +# first and by their increment component second, both treated as unsigned +# 32-bit values. +TIMESTAMP_ORDERING_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "increment_when_seconds_equal", + expression={"$lt": [Timestamp(100, 1), Timestamp(100, 2)]}, + expected=True, + msg="Timestamp ordering should compare increment when seconds are equal", + ), + ExpressionTestCase( + "seconds_before_increment", + expression={"$lt": [Timestamp(100, 2), Timestamp(200, 1)]}, + expected=True, + msg="Timestamp ordering should compare seconds before increment", + ), + ExpressionTestCase( + "zero_timestamp_lowest", + expression={"$lt": [Timestamp(0, 0), Timestamp(0, 1)]}, + expected=True, + msg="Timestamp ordering should place the zero timestamp lowest", + ), + ExpressionTestCase( + "increment_unsigned_high", + expression={"$lt": [Timestamp(1, 1), Timestamp(1, 4_294_967_295)]}, + expected=True, + msg="Timestamp ordering should treat the increment as an unsigned 32-bit value", + ), + ExpressionTestCase( + "seconds_unsigned_high", + expression={"$lt": [Timestamp(1, 1), Timestamp(4_294_967_295, 1)]}, + expected=True, + msg="Timestamp ordering should treat the seconds as an unsigned 32-bit value", + ), + ExpressionTestCase( + "near_max_lt_max_timestamp", + expression={ + "$lt": [ + Timestamp(4_294_967_295, 4_294_967_294), + Timestamp(4_294_967_295, 4_294_967_295), + ] + }, + expected=True, + msg=( + "Timestamp ordering should distinguish the maximum possible" + " timestamp from one below it" + ), + ), +] + +# Property [Timestamp Equality]: Timestamps are equal when both their seconds +# and increment components match. +TIMESTAMP_EQUALITY_TESTS: list[ExpressionTestCase] = [ + ExpressionTestCase( + "same_components_equal", + expression={"$eq": [Timestamp(100, 5), Timestamp(100, 5)]}, + expected=True, + msg="Timestamp equality should hold when both components match", + ), + ExpressionTestCase( + "different_increment_not_equal", + expression={"$eq": [Timestamp(100, 5), Timestamp(100, 6)]}, + expected=False, + msg="Timestamp equality should distinguish different increment components", + ), + ExpressionTestCase( + "different_seconds_not_equal", + expression={"$eq": [Timestamp(100, 5), Timestamp(101, 5)]}, + expected=False, + msg="Timestamp equality should distinguish different seconds components", + ), +] + +TIMESTAMP_COMPARISON_TESTS = TIMESTAMP_ORDERING_TESTS + TIMESTAMP_EQUALITY_TESTS + + +@pytest.mark.parametrize("test", pytest_params(TIMESTAMP_COMPARISON_TESTS)) +def test_timestamp_comparison(collection, test): + """Test Timestamp BSON type comparison semantics.""" + result = execute_expression(collection, test.expression) + assert_expression_result(result, expected=test.expected, msg=test.msg) + + +# Property [Timestamp Round-Trip Fidelity]: Timestamp values survive insert and +# retrieval unchanged. +TIMESTAMP_ROUND_TRIP_TESTS: list[RoundTripTestCase] = [ + RoundTripTestCase( + "small_timestamp", + value=Timestamp(1, 1), + expected=Timestamp(1, 1), + msg="Small non-zero timestamp should survive round-trip", + ), + RoundTripTestCase( + "max_seconds", + value=Timestamp(4_294_967_295, 1), + expected=Timestamp(4_294_967_295, 1), + msg="Maximum seconds component should survive round-trip", + ), + RoundTripTestCase( + "max_increment", + value=Timestamp(1, 4_294_967_295), + expected=Timestamp(1, 4_294_967_295), + msg="Maximum increment component should survive round-trip", + ), + RoundTripTestCase( + "max_timestamp", + value=Timestamp(4_294_967_295, 4_294_967_295), + expected=Timestamp(4_294_967_295, 4_294_967_295), + msg="Maximum possible timestamp should survive round-trip", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TIMESTAMP_ROUND_TRIP_TESTS)) +def test_timestamp_round_trip(collection, test): + """Test Timestamp values survive storage and retrieval unchanged.""" + collection.insert_one({"_id": test.id, "v": test.value}) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": test.id}}) + assertSuccess(result, [{"_id": test.id, "v": test.expected}], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/utils/__init__.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/data-types/bson_types/utils/round_trip_test_case.py b/documentdb_tests/compatibility/tests/core/data-types/bson_types/utils/round_trip_test_case.py new file mode 100644 index 000000000..e93c0a316 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/data-types/bson_types/utils/round_trip_test_case.py @@ -0,0 +1,19 @@ +"""Shared test case for BSON type round-trip tests.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class RoundTripTestCase(BaseTestCase): + """Test case for BSON type storage and retrieval. + + Attributes: + value: The value to insert and retrieve. + """ + + value: Any = None diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_boundary_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_boundary_precision.py index 075c60910..ed50905f6 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_boundary_precision.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_boundary_precision.py @@ -4,7 +4,7 @@ Covers cross-type boundary values, large number precision at double/long boundary, and overflow adjacency. Decimal128 precision and double self-equality are tested in -/core/bson_types/test_bson_types_ordering.py. +/core/data-types/bson_types/. """ import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_numeric_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_numeric_edge_cases.py index b03d2664d..e4b381dd7 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_numeric_edge_cases.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_numeric_edge_cases.py @@ -3,7 +3,7 @@ Covers sign handling, Infinity comparisons, and cross-type Decimal128 Infinity. Numeric equivalence across types, negative zero, and NaN equality are tested in -/core/bson_types/test_bson_types_ordering.py. +/core/data-types/bson_types/. """ import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_same_type_comparisons.py b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_same_type_comparisons.py index ee74dd2c6..de46f7121 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_same_type_comparisons.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/eq/test_eq_same_type_comparisons.py @@ -2,7 +2,7 @@ Tests for $eq same-type comparisons. Covers date, timestamp, ObjectId, BinData, regex, UUID, and large input comparisons. -String comparison semantics are tested in /core/bson_types/test_bson_types_ordering.py. +String comparison semantics are tested in /core/data-types/bson_types/. """ from datetime import datetime, timezone diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_boundary_precision.py b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_boundary_precision.py index 0a9650ab9..3327fa9c3 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_boundary_precision.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_boundary_precision.py @@ -4,7 +4,7 @@ Covers cross-type boundary values, large number precision at double/long boundary, and overflow adjacency. Decimal128 precision and double self-equality are tested in -/core/bson_types/test_bson_types_ordering.py. +/core/data-types/bson_types/. """ import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_numeric_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_numeric_edge_cases.py index 4af09d64a..2ac180287 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_numeric_edge_cases.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_numeric_edge_cases.py @@ -3,7 +3,7 @@ Covers sign handling, Infinity comparisons, and cross-type Decimal128 Infinity. Numeric equivalence across types, negative zero, and NaN equality are tested in -/core/bson_types/test_bson_types_ordering.py. +/core/data-types/bson_types/. """ import pytest diff --git a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_same_type_comparisons.py b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_same_type_comparisons.py index 1a63aed53..fc44b184f 100644 --- a/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_same_type_comparisons.py +++ b/documentdb_tests/compatibility/tests/core/operator/expressions/comparisons/ne/test_ne_same_type_comparisons.py @@ -2,7 +2,7 @@ Tests for $ne same-type comparisons. Covers date, timestamp, ObjectId, BinData, regex, UUID, and large input comparisons. -String comparison semantics are tested in /core/bson_types/test_bson_types_ordering.py. +String comparison semantics are tested in /core/data-types/bson_types/. """ from datetime import datetime, timezone