diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/__init__.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_bson_wiring.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_bson_wiring.py new file mode 100644 index 00000000..97fedf58 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_bson_wiring.py @@ -0,0 +1,157 @@ +""" +Tests for $gte BSON type wiring. + +A representative sample of types to confirm $gte is wired up to the +BSON comparison engine correctly (not exhaustive cross-type matrix). +""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Timestamp +from bson.codec_options import CodecOptions + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="double", + filter={"a": {"$gte": 5.0}}, + doc=[{"_id": 1, "a": 1.0}, {"_id": 2, "a": 5.0}, {"_id": 3, "a": 10.0}], + expected=[{"_id": 2, "a": 5.0}, {"_id": 3, "a": 10.0}], + msg="$gte with double returns docs with value >= 5.0", + ), + QueryTestCase( + id="int", + filter={"a": {"$gte": 5}}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 10}], + expected=[{"_id": 2, "a": 5}, {"_id": 3, "a": 10}], + msg="$gte with int returns docs with value >= 5", + ), + QueryTestCase( + id="long", + filter={"a": {"$gte": Int64(5)}}, + doc=[ + {"_id": 1, "a": Int64(1)}, + {"_id": 2, "a": Int64(5)}, + {"_id": 3, "a": Int64(10)}, + ], + expected=[{"_id": 2, "a": Int64(5)}, {"_id": 3, "a": Int64(10)}], + msg="$gte with long returns docs with value >= 5", + ), + QueryTestCase( + id="decimal128", + filter={"a": {"$gte": Decimal128("5")}}, + doc=[ + {"_id": 1, "a": Decimal128("1")}, + {"_id": 2, "a": Decimal128("5")}, + {"_id": 3, "a": Decimal128("10")}, + ], + expected=[{"_id": 2, "a": Decimal128("5")}, {"_id": 3, "a": Decimal128("10")}], + msg="$gte with decimal128 returns docs with value >= 5", + ), + QueryTestCase( + id="string", + filter={"a": {"$gte": "banana"}}, + doc=[ + {"_id": 1, "a": "apple"}, + {"_id": 2, "a": "banana"}, + {"_id": 3, "a": "cherry"}, + ], + expected=[{"_id": 2, "a": "banana"}, {"_id": 3, "a": "cherry"}], + msg="$gte with string returns docs with value >= 'banana'", + ), + QueryTestCase( + id="date", + filter={"a": {"$gte": datetime(2023, 1, 1, tzinfo=timezone.utc)}}, + doc=[ + {"_id": 1, "a": datetime(2020, 1, 1, tzinfo=timezone.utc)}, + {"_id": 2, "a": datetime(2023, 1, 1, tzinfo=timezone.utc)}, + {"_id": 3, "a": datetime(2025, 1, 1, tzinfo=timezone.utc)}, + ], + expected=[ + {"_id": 2, "a": datetime(2023, 1, 1, tzinfo=timezone.utc)}, + {"_id": 3, "a": datetime(2025, 1, 1, tzinfo=timezone.utc)}, + ], + msg="$gte with date returns docs with equal or later dates", + ), + QueryTestCase( + id="timestamp", + filter={"a": {"$gte": Timestamp(2000, 1)}}, + doc=[ + {"_id": 1, "a": Timestamp(1000, 1)}, + {"_id": 2, "a": Timestamp(2000, 1)}, + {"_id": 3, "a": Timestamp(3000, 1)}, + ], + expected=[{"_id": 2, "a": Timestamp(2000, 1)}, {"_id": 3, "a": Timestamp(3000, 1)}], + msg="$gte with timestamp returns docs with equal or larger timestamp", + ), + QueryTestCase( + id="objectid", + filter={"a": {"$gte": ObjectId("507f1f77bcf86cd799439012")}}, + doc=[ + {"_id": 1, "a": ObjectId("507f1f77bcf86cd799439011")}, + {"_id": 2, "a": ObjectId("507f1f77bcf86cd799439012")}, + {"_id": 3, "a": ObjectId("507f1f77bcf86cd799439013")}, + ], + expected=[ + {"_id": 2, "a": ObjectId("507f1f77bcf86cd799439012")}, + {"_id": 3, "a": ObjectId("507f1f77bcf86cd799439013")}, + ], + msg="$gte with ObjectId returns docs with equal or later ObjectId", + ), + QueryTestCase( + id="boolean", + filter={"a": {"$gte": False}}, + doc=[{"_id": 1, "a": False}, {"_id": 2, "a": True}], + expected=[{"_id": 1, "a": False}, {"_id": 2, "a": True}], + msg="$gte with boolean false returns both true and false", + ), + QueryTestCase( + id="bindata", + filter={"a": {"$gte": Binary(b"\x05\x06", 128)}}, + doc=[ + {"_id": 1, "a": Binary(b"\x01\x02", 128)}, + {"_id": 2, "a": Binary(b"\x05\x06", 128)}, + {"_id": 3, "a": Binary(b"\x09\x0a", 128)}, + ], + expected=[ + {"_id": 2, "a": Binary(b"\x05\x06", 128)}, + {"_id": 3, "a": Binary(b"\x09\x0a", 128)}, + ], + msg="$gte with BinData returns docs with equal or larger binary", + ), + QueryTestCase( + id="minkey", + filter={"a": {"$gte": MinKey()}}, + doc=[{"_id": 1, "a": MinKey()}, {"_id": 2, "a": 1}], + expected=[{"_id": 1, "a": MinKey()}, {"_id": 2, "a": 1}], + msg="$gte with MinKey returns all docs", + ), + QueryTestCase( + id="maxkey", + filter={"a": {"$gte": MaxKey()}}, + doc=[ + {"_id": 1, "a": 1}, + {"_id": 2, "a": "hello"}, + {"_id": 3, "a": MaxKey()}, + ], + expected=[{"_id": 3, "a": MaxKey()}], + msg="$gte with MaxKey returns only MaxKey doc", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TESTS)) +def test_gte_bson_wiring(collection, test): + """Parametrized test for $gte BSON type wiring.""" + collection.insert_many(test.doc) + cmd = {"find": collection.name, "filter": test.filter} + codec = CodecOptions(tz_aware=True, tzinfo=timezone.utc) + result = execute_command(collection, cmd, codec_options=codec) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_edge_cases.py new file mode 100644 index 00000000..8d4a55b1 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_edge_cases.py @@ -0,0 +1,150 @@ +""" +Edge case tests for $gte operator. + +Covers deeply nested field paths with NaN, large array element matching, +empty string ordering, null/missing field handling, and BSON type bracketing. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +MISC_EDGE_CASE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="deeply_nested_field_with_nan", + filter={"a.b.c.d.e": {"$gte": 10}}, + doc=[ + {"_id": 1, "a": {"b": {"c": {"d": {"e": 5}}}}}, + {"_id": 2, "a": {"b": {"c": {"d": {"e": 15}}}}}, + {"_id": 3, "a": {"b": {"c": {"d": {"e": float("nan")}}}}}, + ], + expected=[{"_id": 2, "a": {"b": {"c": {"d": {"e": 15}}}}}], + msg="$gte on deeply nested field path; NaN does not satisfy $gte", + ), + QueryTestCase( + id="large_array_element_match", + filter={"a": {"$gte": 1001}}, + doc=[ + {"_id": 1, "a": list(range(0, 1000)) + [1001]}, + {"_id": 2, "a": list(range(0, 1000))}, + ], + expected=[{"_id": 1, "a": list(range(0, 1000)) + [1001]}], + msg="$gte matches element in a large (1001-element) array", + ), +] + +NULL_MISSING_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="null_query_matches_null_and_missing", + filter={"a": {"$gte": None}}, + doc=[{"_id": 1, "a": 5}, {"_id": 2, "a": None}, {"_id": 3}], + expected=[{"_id": 2, "a": None}, {"_id": 3}], + msg="$gte null matches null and missing fields (null >= null)", + ), + QueryTestCase( + id="null_field_not_gte_numeric", + filter={"a": {"$gte": 5}}, + doc=[{"_id": 1, "a": None}], + expected=[], + msg="null field does not match $gte with numeric query", + ), +] + +TYPE_BRACKETING_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="gte_false_no_cross_type_int_zero", + filter={"a": {"$gte": False}}, + doc=[{"_id": 1, "a": 0}], + expected=[], + msg="int 0 does not match $gte false (different BSON types)", + ), + QueryTestCase( + id="gte_true_no_cross_type_int_one", + filter={"a": {"$gte": True}}, + doc=[{"_id": 1, "a": 1}], + expected=[], + msg="int 1 does not match $gte true (different BSON types)", + ), + QueryTestCase( + id="gte_int_zero_no_cross_type_false", + filter={"a": {"$gte": 0}}, + doc=[{"_id": 1, "a": False}], + expected=[], + msg="false does not match $gte 0 (different BSON types)", + ), + QueryTestCase( + id="gte_int_one_no_cross_type_true", + filter={"a": {"$gte": 1}}, + doc=[{"_id": 1, "a": True}], + expected=[], + msg="true does not match $gte 1 (different BSON types)", + ), + QueryTestCase( + id="gte_int_zero_no_cross_type_string", + filter={"a": {"$gte": 0}}, + doc=[{"_id": 1, "a": "0"}], + expected=[], + msg="string '0' does not match $gte int 0 (different BSON types)", + ), + QueryTestCase( + id="gte_string_no_cross_type_int", + filter={"a": {"$gte": "0"}}, + doc=[{"_id": 1, "a": 0}], + expected=[], + msg="int 0 does not match $gte string '0' (different BSON types)", + ), + QueryTestCase( + id="gte_null_no_cross_type_string", + filter={"a": {"$gte": None}}, + doc=[{"_id": 1, "a": "hello"}], + expected=[], + msg="string does not match $gte null (different BSON types)", + ), + QueryTestCase( + id="gte_false_no_cross_type_null", + filter={"a": {"$gte": False}}, + doc=[{"_id": 1, "a": None}], + expected=[], + msg="null does not match $gte false (different BSON types)", + ), + QueryTestCase( + id="gte_int_zero_no_cross_type_null", + filter={"a": {"$gte": 0}}, + doc=[{"_id": 1, "a": None}], + expected=[], + msg="null does not match $gte int 0 (different BSON types)", + ), + QueryTestCase( + id="gte_null_no_cross_type_bool", + filter={"a": {"$gte": None}}, + doc=[{"_id": 1, "a": True}], + expected=[], + msg="bool true does not match $gte null (different BSON types)", + ), + QueryTestCase( + id="bool_false_not_gte_true", + filter={"a": {"$gte": True}}, + doc=[{"_id": 1, "a": False}], + expected=[], + msg="bool false does not match $gte true (false < true)", + ), +] + +ALL_PARAMETRIZED_TESTS = MISC_EDGE_CASE_TESTS + NULL_MISSING_TESTS + TYPE_BRACKETING_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_PARAMETRIZED_TESTS)) +def test_gte_edge_cases(collection, test): + """Parametrized test for $gte edge cases. + + Covers nested fields, large arrays, null/missing, and type bracketing. + """ + collection.insert_many(test.doc) + cmd = {"find": collection.name, "filter": test.filter} + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_field_lookup.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_field_lookup.py new file mode 100644 index 00000000..c48b5275 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_field_lookup.py @@ -0,0 +1,121 @@ +""" +Tests for $gte field lookup and array value comparison. + +Covers array element matching, array index access, +numeric key disambiguation, _id with ObjectId, array of embedded documents, +whole-array comparison, and empty array behavior. +""" + +import pytest +from bson import ObjectId + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +FIELD_LOOKUP_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="array_element_matching", + filter={"a": {"$gte": 12}}, + doc=[{"_id": 1, "a": [3, 7, 12]}, {"_id": 2, "a": [1, 5]}], + expected=[{"_id": 1, "a": [3, 7, 12]}], + msg="$gte matches if any array element satisfies condition (including equal)", + ), + QueryTestCase( + id="array_no_element_match", + filter={"a": {"$gte": 15}}, + doc=[{"_id": 1, "a": [1, 5]}], + expected=[], + msg="$gte on array with no element greater than or equal to query", + ), + QueryTestCase( + id="array_index_zero", + filter={"arr.0": {"$gte": 25}}, + doc=[{"_id": 1, "arr": [25, 5]}, {"_id": 2, "arr": [10, 30]}], + expected=[{"_id": 1, "arr": [25, 5]}], + msg="$gte on array index 0 matches equal value", + ), + QueryTestCase( + id="numeric_key_on_object", + filter={"a.0.b": {"$gte": 10}}, + doc=[{"_id": 1, "a": {"0": {"b": 10}}}, {"_id": 2, "a": {"0": {"b": 3}}}], + expected=[{"_id": 1, "a": {"0": {"b": 10}}}], + msg="$gte with numeric key on object (not array)", + ), + QueryTestCase( + id="id_objectid", + filter={"_id": {"$gte": ObjectId("507f1f77bcf86cd799439012")}}, + doc=[ + {"_id": ObjectId("507f1f77bcf86cd799439011"), "a": 1}, + {"_id": ObjectId("507f1f77bcf86cd799439012"), "a": 2}, + {"_id": ObjectId("507f1f77bcf86cd799439013"), "a": 3}, + ], + expected=[ + {"_id": ObjectId("507f1f77bcf86cd799439012"), "a": 2}, + {"_id": ObjectId("507f1f77bcf86cd799439013"), "a": 3}, + ], + msg="$gte on _id with ObjectId includes equal", + ), + QueryTestCase( + id="array_of_embedded_docs_dot_notation", + filter={"a.b": {"$gte": 15}}, + doc=[ + {"_id": 1, "a": [{"b": 3}, {"b": 15}]}, + {"_id": 2, "a": [{"b": 1}, {"b": 5}]}, + ], + expected=[{"_id": 1, "a": [{"b": 3}, {"b": 15}]}], + msg="$gte on array of embedded docs via dot notation matches equal", + ), +] + +ARRAY_VALUE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="array_gte_array_element_comparison", + filter={"a": {"$gte": [1, 2]}}, + doc=[{"_id": 1, "a": [1, 2]}, {"_id": 2, "a": [1, 3]}], + expected=[{"_id": 1, "a": [1, 2]}, {"_id": 2, "a": [1, 3]}], + msg="$gte array includes equal and greater arrays", + ), + QueryTestCase( + id="longer_array_gte_shorter_prefix", + filter={"a": {"$gte": [1, 2]}}, + doc=[{"_id": 1, "a": [1, 2, 3]}], + expected=[{"_id": 1, "a": [1, 2, 3]}], + msg="$gte longer array is greater than or equal to shorter prefix", + ), + QueryTestCase( + id="empty_array_gte_empty_array", + filter={"a": {"$gte": []}}, + doc=[{"_id": 1, "a": []}, {"_id": 2, "a": [1]}], + expected=[{"_id": 1, "a": []}, {"_id": 2, "a": [1]}], + msg="$gte empty array matches empty and non-empty arrays", + ), + QueryTestCase( + id="array_with_null_element_gte_scalar", + filter={"a": {"$gte": 5}}, + doc=[{"_id": 1, "a": [None, 5]}], + expected=[{"_id": 1, "a": [None, 5]}], + msg="$gte matches array with element 5 >= 5", + ), + QueryTestCase( + id="empty_array_not_gte_scalar", + filter={"a": {"$gte": 5}}, + doc=[{"_id": 1, "a": []}], + expected=[], + msg="$gte empty array does not match scalar query", + ), +] + +ALL_TESTS = FIELD_LOOKUP_TESTS + ARRAY_VALUE_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_gte_field_lookup(collection, test): + """Parametrized test for $gte field lookup and array comparison.""" + collection.insert_many(test.doc) + cmd = {"find": collection.name, "filter": test.filter} + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_numeric_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_numeric_edge_cases.py new file mode 100644 index 00000000..7da20fbd --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_numeric_edge_cases.py @@ -0,0 +1,205 @@ +""" +Tests for $gte numeric edge cases. + +Covers cross-type numeric comparison, non-matching cross-type comparison, +INT64 and INT32 boundary values, NaN (including self-equality), infinity, +negative zero, and precision loss. +""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +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_NEGATIVE_INFINITY, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_PRECISION_LOSS, + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, + INT32_MAX, + INT32_MIN, + INT32_MIN_PLUS_1, + INT64_MAX, +) + +CROSS_TYPE_NUMERIC_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="int_gte_double", + filter={"a": {"$gte": 5.0}}, + doc=[{"_id": 1, "a": 5}, {"_id": 2, "a": 4}], + expected=[{"_id": 1, "a": 5}], + msg="int field >= double query via type bracketing (equal match)", + ), + QueryTestCase( + id="double_gte_int", + filter={"a": {"$gte": 5}}, + doc=[{"_id": 1, "a": 5.0}, {"_id": 2, "a": 4.5}], + expected=[{"_id": 1, "a": 5.0}], + msg="double field >= int query via type bracketing (equal match)", + ), +] + +NON_MATCHING_CROSS_TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="string_field_not_gte_int_query", + filter={"a": {"$gte": 10}}, + doc=[{"_id": 1, "a": "hello"}], + expected=[], + msg="string field does not match $gte with int query", + ), +] + +BOUNDARY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="boundary_int64_max_equal", + filter={"a": {"$gte": INT64_MAX}}, + doc=[{"_id": 1, "a": INT64_MAX}], + expected=[{"_id": 1, "a": INT64_MAX}], + msg="$gte with INT64_MAX equal value matches", + ), + QueryTestCase( + id="boundary_int64_max_greater", + filter={"a": {"$gte": INT64_MAX - 1}}, + doc=[{"_id": 1, "a": INT64_MAX}], + expected=[{"_id": 1, "a": INT64_MAX}], + msg="$gte with INT64_MAX matches value one greater", + ), +] + +NAN_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="nan_field_not_gte_number", + filter={"a": {"$gte": 5}}, + doc=[{"_id": 1, "a": FLOAT_NAN}], + expected=[], + msg="NaN field does not match $gte 5", + ), + QueryTestCase( + id="number_not_gte_nan_query", + filter={"a": {"$gte": FLOAT_NAN}}, + doc=[{"_id": 1, "a": 5}], + expected=[], + msg="numeric field does not match $gte NaN", + ), + QueryTestCase( + id="nan_not_gte_negative_infinity", + filter={"a": {"$gte": FLOAT_NEGATIVE_INFINITY}}, + doc=[{"_id": 1, "a": FLOAT_NAN}], + expected=[], + msg="NaN does not match $gte -Infinity (NaN < -Infinity in BSON ordering)", + ), +] + +INFINITY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="infinity_gte_number", + filter={"a": {"$gte": 999999}}, + doc=[{"_id": 1, "a": FLOAT_INFINITY}], + expected=[{"_id": 1, "a": FLOAT_INFINITY}], + msg="Infinity is greater than or equal to large number", + ), + QueryTestCase( + id="number_not_gte_infinity", + filter={"a": {"$gte": FLOAT_INFINITY}}, + doc=[{"_id": 1, "a": -999999}], + expected=[], + msg="negative number is not greater than or equal to Infinity", + ), +] + +NEGATIVE_ZERO_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="neg_zero_gte_pos_zero", + filter={"a": {"$gte": 0.0}}, + doc=[{"_id": 1, "a": DOUBLE_NEGATIVE_ZERO}], + expected=[{"_id": 1, "a": DOUBLE_NEGATIVE_ZERO}], + msg="-0.0 is >= 0.0 (they are equal)", + ), +] + +PRECISION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="long_2e53_plus1_gte_double_2e53", + filter={"a": {"$gte": float(DOUBLE_MAX_SAFE_INTEGER)}}, + doc=[{"_id": 1, "a": Int64(DOUBLE_PRECISION_LOSS)}], + expected=[{"_id": 1, "a": Int64(DOUBLE_PRECISION_LOSS)}], + msg="Long(2^53+1) is >= double(2^53) — precision loss boundary", + ), + QueryTestCase( + id="double_rounded_up_gte_int64_max", + filter={"a": {"$gte": INT64_MAX}}, + doc=[{"_id": 1, "a": float(INT64_MAX)}], + expected=[{"_id": 1, "a": float(INT64_MAX)}], + msg="Rounded-up double representation is >= INT64_MAX", + ), +] + +DECIMAL128_INFINITY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="decimal128_number_not_gte_inf", + filter={"a": {"$gte": DECIMAL128_INFINITY}}, + doc=[{"_id": 1, "a": Decimal128("0")}], + expected=[], + msg="Decimal128 0 is not >= Decimal128 Infinity", + ), + QueryTestCase( + id="decimal128_number_gte_neg_inf", + filter={"a": {"$gte": DECIMAL128_NEGATIVE_INFINITY}}, + doc=[{"_id": 1, "a": Decimal128("-999999")}], + expected=[{"_id": 1, "a": Decimal128("-999999")}], + msg="Decimal128 number is >= Decimal128 -Infinity", + ), +] + +INT32_BOUNDARY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="int32_max_equal_match", + filter={"a": {"$gte": INT32_MAX}}, + doc=[{"_id": 1, "a": INT32_MAX}, {"_id": 2, "a": INT32_MAX - 1}], + expected=[{"_id": 1, "a": INT32_MAX}], + msg="$gte INT32_MAX matches only equal value", + ), + QueryTestCase( + id="int32_min_equal_match", + filter={"a": {"$gte": INT32_MIN}}, + doc=[{"_id": 1, "a": INT32_MIN}, {"_id": 2, "a": INT32_MIN - 1}], + expected=[{"_id": 1, "a": INT32_MIN}], + msg="$gte INT32_MIN matches equal value, not one below", + ), + QueryTestCase( + id="int32_min_plus1_greater_match", + filter={"a": {"$gte": INT32_MIN}}, + doc=[{"_id": 1, "a": INT32_MIN_PLUS_1}], + expected=[{"_id": 1, "a": INT32_MIN_PLUS_1}], + msg="INT32_MIN + 1 matches $gte INT32_MIN", + ), +] + +ALL_TESTS = ( + CROSS_TYPE_NUMERIC_TESTS + + NON_MATCHING_CROSS_TYPE_TESTS + + BOUNDARY_TESTS + + NAN_TESTS + + INFINITY_TESTS + + NEGATIVE_ZERO_TESTS + + PRECISION_TESTS + + DECIMAL128_INFINITY_TESTS + + INT32_BOUNDARY_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_gte_numeric_edge_cases(collection, test): + """Parametrized test for $gte numeric edge cases.""" + collection.insert_many(test.doc) + cmd = {"find": collection.name, "filter": test.filter} + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_value_matching.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_value_matching.py new file mode 100644 index 00000000..fefe56e2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gte/test_gte_value_matching.py @@ -0,0 +1,176 @@ +""" +Tests for $gte value matching — arrays, objects, dates, timestamps, and strings. + +Covers array comparison semantics (first-element-wins, nested traversal), +object/subdocument comparison (field values, empty, NaN sort order), +date ordering across epoch boundary, timestamp ordering, +Date vs Timestamp type distinction, and string lexicographic/case comparison. +""" + +import pytest +from bson import SON, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +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 ( + DATE_BEFORE_EPOCH, + DATE_EPOCH, + TS_EPOCH, +) + +ARRAY_COMPARISON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="array_first_element_decides", + filter={"a": {"$gte": [1, 2, 3]}}, + doc=[{"_id": 1, "a": [3, 2, 1]}, {"_id": 2, "a": [1, 2, 3]}], + expected=[{"_id": 1, "a": [3, 2, 1]}, {"_id": 2, "a": [1, 2, 3]}], + msg="$gte includes equal array and array with greater first element", + ), + QueryTestCase( + id="nested_array_not_traversed", + filter={"a": {"$gte": 5}}, + doc=[{"_id": 1, "a": [[6, 7], [8, 9]]}], + expected=[], + msg="$gte scalar does not traverse nested arrays", + ), +] + +OBJECT_COMPARISON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="subdocument_field_value_gte", + filter={"a": {"$gte": SON([("x", 2), ("y", 1)])}}, + doc=[ + {"_id": 1, "a": SON([("x", 3), ("y", 1)])}, + {"_id": 2, "a": SON([("x", 2), ("y", 1)])}, + {"_id": 3, "a": SON([("x", 1), ("y", 1)])}, + ], + expected=[{"_id": 1, "a": {"x": 3, "y": 1}}, {"_id": 2, "a": {"x": 2, "y": 1}}], + msg="$gte subdocument includes equal and greater", + ), + QueryTestCase( + id="nonempty_doc_gte_empty", + filter={"a": {"$gte": {}}}, + doc=[{"_id": 1, "a": {}}, {"_id": 2, "a": {"x": 1}}], + expected=[{"_id": 1, "a": {}}, {"_id": 2, "a": {"x": 1}}], + msg="$gte empty document matches empty and non-empty", + ), + QueryTestCase( + id="subdocument_numeric_gte_nan_subdocument", + filter={"a": {"$gte": SON([("x", float("nan"))])}}, + doc=[ + {"_id": 1, "a": SON([("x", 5)])}, + ], + expected=[{"_id": 1, "a": {"x": 5}}], + msg="$gte NaN subdocument matches greater numeric (NaN sorts lowest)", + ), +] + +DATE_COMPARISON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="epoch_gte_pre_epoch", + filter={"a": {"$gte": DATE_BEFORE_EPOCH}}, + doc=[{"_id": 1, "a": DATE_BEFORE_EPOCH}, {"_id": 2, "a": DATE_EPOCH}], + expected=[{"_id": 1, "a": DATE_BEFORE_EPOCH}, {"_id": 2, "a": DATE_EPOCH}], + msg="$gte pre-epoch returns both equal and later dates", + ), + QueryTestCase( + id="date_equal_matches_gte", + filter={"a": {"$gte": DATE_BEFORE_EPOCH}}, + doc=[{"_id": 1, "a": DATE_BEFORE_EPOCH}], + expected=[{"_id": 1, "a": DATE_BEFORE_EPOCH}], + msg="equal date matches $gte", + ), + QueryTestCase( + id="ts_seconds_then_increment", + filter={"a": {"$gte": Timestamp(100, 1)}}, + doc=[ + {"_id": 1, "a": Timestamp(100, 1)}, + {"_id": 2, "a": Timestamp(100, 2)}, + {"_id": 3, "a": Timestamp(99, 999)}, + ], + expected=[ + {"_id": 1, "a": Timestamp(100, 1)}, + {"_id": 2, "a": Timestamp(100, 2)}, + ], + msg="Timestamp orders by seconds first, then increment", + ), + QueryTestCase( + id="date_not_gte_timestamp", + filter={"a": {"$gte": Timestamp(0, 1)}}, + doc=[{"_id": 1, "a": DATE_EPOCH}], + expected=[], + msg="Date field does not match $gte with Timestamp query (different BSON types)", + ), + QueryTestCase( + id="timestamp_not_gte_date", + filter={"a": {"$gte": DATE_EPOCH}}, + doc=[{"_id": 1, "a": TS_EPOCH}], + expected=[], + msg="Timestamp field does not match $gte with Date query (different BSON types)", + ), +] + +STRING_COMPARISON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="prefix_shorter_not_gte_longer", + filter={"a": {"$gte": "abcdef"}}, + doc=[{"_id": 1, "a": "abc"}], + expected=[], + msg="prefix 'abc' is not >= longer string 'abcdef'", + ), + QueryTestCase( + id="prefix_longer_gte_shorter", + filter={"a": {"$gte": "abc"}}, + doc=[{"_id": 1, "a": "abcdef"}], + expected=[{"_id": 1, "a": "abcdef"}], + msg="longer string 'abcdef' is >= prefix 'abc'", + ), + QueryTestCase( + id="empty_string_gte_empty_string", + filter={"a": {"$gte": ""}}, + doc=[{"_id": 1, "a": ""}, {"_id": 2, "a": "a"}], + expected=[{"_id": 1, "a": ""}, {"_id": 2, "a": "a"}], + msg="empty string and non-empty both match $gte empty string", + ), + QueryTestCase( + id="nonempty_not_gte_when_less", + filter={"a": {"$gte": "b"}}, + doc=[{"_id": 1, "a": ""}], + expected=[], + msg="empty string does not match $gte 'b'", + ), + QueryTestCase( + id="case_sensitivity_upper_less_than_lower", + filter={"a": {"$gte": "a"}}, + doc=[{"_id": 1, "a": "A"}, {"_id": 2, "a": "a"}, {"_id": 3, "a": "b"}], + expected=[{"_id": 2, "a": "a"}, {"_id": 3, "a": "b"}], + msg="binary comparison: 'A' (0x41) < 'a' (0x61), so 'A' not >= 'a'", + ), + QueryTestCase( + id="case_sensitivity_lower_gte_upper", + filter={"a": {"$gte": "A"}}, + doc=[{"_id": 1, "a": "A"}, {"_id": 2, "a": "a"}, {"_id": 3, "a": "Z"}], + expected=[{"_id": 1, "a": "A"}, {"_id": 2, "a": "a"}, {"_id": 3, "a": "Z"}], + msg="binary comparison: 'a' and 'Z' are both >= 'A'", + ), +] + +ALL_TESTS = ( + ARRAY_COMPARISON_TESTS + + OBJECT_COMPARISON_TESTS + + DATE_COMPARISON_TESTS + + STRING_COMPARISON_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_gte_value_matching(collection, test): + """Parametrized test for $gte value matching.""" + collection.insert_many(test.doc) + cmd = {"find": collection.name, "filter": test.filter} + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/test_comparison_nan_equality.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/test_comparison_nan_equality.py new file mode 100644 index 00000000..1c192673 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/test_comparison_nan_equality.py @@ -0,0 +1,74 @@ +""" +Tests for NaN equality behavior across comparison query operators. + +Verifies that NaN == NaN for $eq, $gte, $lte with both float and Decimal128 NaN. +""" + +import pytest +from bson import Decimal128 + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccessNaN +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +FLOAT_NAN = float("nan") +DECIMAL128_NAN = Decimal128("NaN") + +NAN_EQUALITY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="eq_float_nan", + filter={"a": {"$eq": FLOAT_NAN}}, + doc=[{"_id": 1, "a": FLOAT_NAN}, {"_id": 2, "a": 1}], + expected=[{"_id": 1, "a": FLOAT_NAN}], + msg="$eq matches float NaN to float NaN", + ), + QueryTestCase( + id="eq_decimal128_nan", + filter={"a": {"$eq": DECIMAL128_NAN}}, + doc=[{"_id": 1, "a": DECIMAL128_NAN}, {"_id": 2, "a": Decimal128("1")}], + expected=[{"_id": 1, "a": DECIMAL128_NAN}], + msg="$eq matches Decimal128 NaN to Decimal128 NaN", + ), + QueryTestCase( + id="gte_float_nan", + filter={"a": {"$gte": FLOAT_NAN}}, + doc=[{"_id": 1, "a": FLOAT_NAN}, {"_id": 2, "a": 1}], + expected=[{"_id": 1, "a": FLOAT_NAN}], + msg="$gte matches float NaN to float NaN (NaN == NaN)", + ), + QueryTestCase( + id="gte_decimal128_nan", + filter={"a": {"$gte": DECIMAL128_NAN}}, + doc=[{"_id": 1, "a": DECIMAL128_NAN}, {"_id": 2, "a": Decimal128("1")}], + expected=[{"_id": 1, "a": DECIMAL128_NAN}], + msg="$gte matches Decimal128 NaN to Decimal128 NaN (NaN == NaN)", + ), + QueryTestCase( + id="lte_float_nan", + filter={"a": {"$lte": FLOAT_NAN}}, + doc=[{"_id": 1, "a": FLOAT_NAN}, {"_id": 2, "a": 1}], + expected=[{"_id": 1, "a": FLOAT_NAN}], + msg="$lte matches float NaN to float NaN (NaN == NaN)", + ), + QueryTestCase( + id="lte_decimal128_nan", + filter={"a": {"$lte": DECIMAL128_NAN}}, + doc=[{"_id": 1, "a": DECIMAL128_NAN}, {"_id": 2, "a": Decimal128("1")}], + expected=[{"_id": 1, "a": DECIMAL128_NAN}], + msg="$lte matches Decimal128 NaN to Decimal128 NaN (NaN == NaN)", + ), +] + + +ALL_TESTS = NAN_EQUALITY_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_comparison_nan_equality(collection, test): + """Test NaN equality across $eq, $gte, $lte for float and Decimal128.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccessNaN(result, test.expected, ignore_doc_order=True)