From e8ba9710f9ef6cecd45216f882cc3d5b394c62ec Mon Sep 17 00:00:00 2001 From: Victor Tsang Date: Wed, 15 Apr 2026 17:20:49 -0700 Subject: [PATCH 1/3] Added query tests for $gt Signed-off-by: Victor Tsang --- .../operator/query/comparison/gt/__init__.py | 0 .../comparison/gt/test_gt_bson_wiring.py | 146 +++++++++ .../query/comparison/gt/test_gt_edge_cases.py | 137 ++++++++ .../comparison/gt/test_gt_field_lookup.py | 130 ++++++++ .../gt/test_gt_numeric_edge_cases.py | 306 ++++++++++++++++++ .../comparison/gt/test_gt_value_matching.py | 136 ++++++++ 6 files changed, 855 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_bson_wiring.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_edge_cases.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_field_lookup.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_numeric_edge_cases.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_value_matching.py diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/__init__.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_bson_wiring.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_bson_wiring.py new file mode 100644 index 00000000..5d355765 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_bson_wiring.py @@ -0,0 +1,146 @@ +""" +Tests for $gt BSON type wiring. + +A representative sample of types to confirm $gt 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": {"$gt": 5.0}}, + doc=[{"_id": 1, "a": 1.0}, {"_id": 2, "a": 5.0}, {"_id": 3, "a": 10.0}], + expected=[{"_id": 3, "a": 10.0}], + msg="$gt with double returns docs with value > 5.0", + ), + QueryTestCase( + id="int", + filter={"a": {"$gt": 5}}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 10}], + expected=[{"_id": 3, "a": 10}], + msg="$gt with int returns docs with value > 5", + ), + QueryTestCase( + id="long", + filter={"a": {"$gt": Int64(5)}}, + doc=[ + {"_id": 1, "a": Int64(1)}, + {"_id": 2, "a": Int64(5)}, + {"_id": 3, "a": Int64(10)}, + ], + expected=[{"_id": 3, "a": Int64(10)}], + msg="$gt with long returns docs with value > 5", + ), + QueryTestCase( + id="decimal128", + filter={"a": {"$gt": Decimal128("5")}}, + doc=[ + {"_id": 1, "a": Decimal128("1")}, + {"_id": 2, "a": Decimal128("5")}, + {"_id": 3, "a": Decimal128("10")}, + ], + expected=[{"_id": 3, "a": Decimal128("10")}], + msg="$gt with decimal128 returns docs with value > 5", + ), + QueryTestCase( + id="string", + filter={"a": {"$gt": "banana"}}, + doc=[ + {"_id": 1, "a": "apple"}, + {"_id": 2, "a": "banana"}, + {"_id": 3, "a": "cherry"}, + ], + expected=[{"_id": 3, "a": "cherry"}], + msg="$gt with string returns docs with value > 'banana'", + ), + QueryTestCase( + id="date", + filter={"a": {"$gt": 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": 3, "a": datetime(2025, 1, 1, tzinfo=timezone.utc)}], + msg="$gt with date returns docs with later dates", + ), + QueryTestCase( + id="timestamp", + filter={"a": {"$gt": Timestamp(2000, 1)}}, + doc=[ + {"_id": 1, "a": Timestamp(1000, 1)}, + {"_id": 2, "a": Timestamp(3000, 1)}, + ], + expected=[{"_id": 2, "a": Timestamp(3000, 1)}], + msg="$gt with timestamp returns docs with larger timestamp", + ), + QueryTestCase( + id="objectid", + filter={"a": {"$gt": ObjectId("507f1f77bcf86cd799439012")}}, + doc=[ + {"_id": 1, "a": ObjectId("507f1f77bcf86cd799439011")}, + {"_id": 2, "a": ObjectId("507f1f77bcf86cd799439013")}, + ], + expected=[{"_id": 2, "a": ObjectId("507f1f77bcf86cd799439013")}], + msg="$gt with ObjectId returns docs with later ObjectId", + ), + QueryTestCase( + id="boolean", + filter={"a": {"$gt": False}}, + doc=[{"_id": 1, "a": False}, {"_id": 2, "a": True}], + expected=[{"_id": 2, "a": True}], + msg="$gt with boolean false returns doc with true", + ), + QueryTestCase( + id="bindata", + filter={"a": {"$gt": Binary(b"\x05\x06", 128)}}, + doc=[ + {"_id": 1, "a": Binary(b"\x01\x02", 128)}, + {"_id": 2, "a": Binary(b"\x09\x0a", 128)}, + ], + expected=[{"_id": 2, "a": Binary(b"\x09\x0a", 128)}], + msg="$gt with BinData returns docs with larger binary", + ), + QueryTestCase( + id="minkey", + filter={"a": {"$gt": MinKey()}}, + doc=[{"_id": 1, "a": MinKey()}, {"_id": 2, "a": 1}], + expected=[{"_id": 2, "a": 1}], + msg="$gt with MinKey returns all non-MinKey docs", + ), + QueryTestCase( + id="maxkey", + filter={"a": {"$gt": MaxKey()}}, + doc=[ + {"_id": 1, "a": 1}, + {"_id": 2, "a": "hello"}, + {"_id": 3, "a": MaxKey()}, + ], + expected=[], + msg="$gt with MaxKey returns nothing", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TESTS)) +def test_gt_bson_wiring(collection, test): + """Parametrized test for $gt BSON type wiring.""" + collection.insert_many(test.doc) + codec = CodecOptions(tz_aware=True, tzinfo=timezone.utc) + result = execute_command( + collection, {"find": collection.name, "filter": test.filter}, codec_options=codec + ) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_edge_cases.py new file mode 100644 index 00000000..972c35d7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_edge_cases.py @@ -0,0 +1,137 @@ +""" +Edge case tests for $gt operator. + +Covers deeply nested field paths with NaN, large array element matching, +empty string ordering, null/missing field handling, and cross-type bracketing. +""" + +from datetime import datetime, timezone + +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": {"$gt": 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="$gt on deeply nested field path; NaN does not satisfy $gt", + ), + QueryTestCase( + id="large_array_element_match", + filter={"a": {"$gt": 1000}}, + 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="$gt matches element in a large (1001-element) array", + ), + QueryTestCase( + id="empty_string_gt_nothing", + filter={"a": {"$gt": ""}}, + doc=[{"_id": 1, "a": ""}, {"_id": 2, "a": "a"}], + expected=[{"_id": 2, "a": "a"}], + msg="non-empty string is greater than empty string", + ), +] + +NULL_MISSING_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="gt_null_query_returns_nothing", + filter={"a": {"$gt": None}}, + doc=[{"_id": 1, "a": 5}, {"_id": 2, "a": None}, {"_id": 3}], + expected=[], + msg="$gt null returns no documents", + ), + QueryTestCase( + id="null_field_not_gt_numeric_query", + filter={"a": {"$gt": 5}}, + doc=[{"_id": 1, "a": None}], + expected=[], + msg="null field does not match $gt with numeric query", + ), + QueryTestCase( + id="missing_field_not_gt_numeric_query", + filter={"a": {"$gt": 5}}, + doc=[{"_id": 1, "b": 10}], + expected=[], + msg="missing field does not match $gt numeric query", + ), + QueryTestCase( + id="gt_null_on_missing_field_no_match", + filter={"a": {"$gt": None}}, + doc=[{"_id": 1, "b": 10}], + expected=[], + msg="$gt null on missing field returns no match", + ), + QueryTestCase( + id="gt_null_on_non_null_field_no_match", + filter={"a": {"$gt": None}}, + doc=[{"_id": 1, "a": 5}], + expected=[], + msg="$gt null on non-null field returns no match (type bracketing)", + ), +] + +TYPE_BRACKETING_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="bool_false_field_not_gt_int_0_query", + filter={"a": {"$gt": 0}}, + doc=[{"_id": 1, "a": False}], + expected=[], + msg="boolean false is not > int 0 (different type brackets)", + ), + QueryTestCase( + id="bool_true_field_not_gt_int_1_query", + filter={"a": {"$gt": 1}}, + doc=[{"_id": 1, "a": True}], + expected=[], + msg="boolean true is not > int 1 (different type brackets)", + ), + QueryTestCase( + id="date_field_not_gt_numeric_query", + filter={"a": {"$gt": 0}}, + doc=[{"_id": 1, "a": datetime(2025, 1, 1, tzinfo=timezone.utc)}], + expected=[], + msg="date field does not match $gt numeric query (different type brackets)", + ), + QueryTestCase( + id="null_field_not_gt_string_query", + filter={"a": {"$gt": "abc"}}, + doc=[{"_id": 1, "a": None}], + expected=[], + msg="null field does not match $gt string query (different type brackets)", + ), + QueryTestCase( + id="object_not_gt_null", + filter={"a": {"$gt": None}}, + doc=[{"_id": 1, "a": {}}], + expected=[], + msg="empty object does not match $gt null (different BSON types)", + ), +] + +ALL_PARAMETRIZED_TESTS = MISC_EDGE_CASE_TESTS + NULL_MISSING_TESTS + TYPE_BRACKETING_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_PARAMETRIZED_TESTS)) +def test_gt_edge_cases(collection, test): + """Parametrized test for $gt edge cases. + + Covers nested fields, large arrays, null/missing, and type bracketing. + """ + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_field_lookup.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_field_lookup.py new file mode 100644 index 00000000..db8d95c0 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_field_lookup.py @@ -0,0 +1,130 @@ +""" +Tests for $gt 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": {"$gt": 10}}, + doc=[{"_id": 1, "a": [3, 7, 12]}, {"_id": 2, "a": [1, 5]}], + expected=[{"_id": 1, "a": [3, 7, 12]}], + msg="$gt matches if any array element satisfies condition", + ), + QueryTestCase( + id="array_no_element_match", + filter={"a": {"$gt": 15}}, + doc=[{"_id": 1, "a": [1, 5]}], + expected=[], + msg="$gt on array with no element greater than query", + ), + QueryTestCase( + id="array_index_zero", + filter={"arr.0": {"$gt": 20}}, + doc=[{"_id": 1, "arr": [25, 5]}, {"_id": 2, "arr": [10, 30]}], + expected=[{"_id": 1, "arr": [25, 5]}], + msg="$gt on array index 0", + ), + QueryTestCase( + id="numeric_key_on_object", + filter={"a.0.b": {"$gt": 5}}, + doc=[{"_id": 1, "a": {"0": {"b": 10}}}, {"_id": 2, "a": {"0": {"b": 3}}}], + expected=[{"_id": 1, "a": {"0": {"b": 10}}}], + msg="$gt with numeric key on object (not array)", + ), + QueryTestCase( + id="id_objectid", + filter={"_id": {"$gt": ObjectId("507f1f77bcf86cd799439012")}}, + doc=[ + {"_id": ObjectId("507f1f77bcf86cd799439011"), "a": 1}, + {"_id": ObjectId("507f1f77bcf86cd799439013"), "a": 2}, + ], + expected=[{"_id": ObjectId("507f1f77bcf86cd799439013"), "a": 2}], + msg="$gt on _id with ObjectId", + ), + QueryTestCase( + id="array_of_embedded_docs_dot_notation", + filter={"a.b": {"$gt": 10}}, + doc=[ + {"_id": 1, "a": [{"b": 3}, {"b": 15}]}, + {"_id": 2, "a": [{"b": 1}, {"b": 5}]}, + ], + expected=[{"_id": 1, "a": [{"b": 3}, {"b": 15}]}], + msg="$gt on array of embedded docs via dot notation", + ), + QueryTestCase( + id="nested_array_double_index", + filter={"a.0.0": {"$gt": 5}}, + doc=[{"_id": 1, "a": [[10, 2], [3, 4]]}, {"_id": 2, "a": [[1, 2], [3, 4]]}], + expected=[{"_id": 1, "a": [[10, 2], [3, 4]]}], + msg="$gt on a.0.0 resolves nested array index to first element of first sub-array", + ), +] + +ARRAY_VALUE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="array_gt_array_element_comparison", + filter={"a": {"$gt": [1, 2]}}, + doc=[{"_id": 1, "a": [1, 3]}], + expected=[{"_id": 1, "a": [1, 3]}], + msg="$gt array [1,3] > [1,2] via array comparison", + ), + QueryTestCase( + id="longer_array_gt_shorter_prefix", + filter={"a": {"$gt": [1, 2]}}, + doc=[{"_id": 1, "a": [1, 2, 3]}], + expected=[{"_id": 1, "a": [1, 2, 3]}], + msg="$gt longer array is greater than shorter prefix", + ), + QueryTestCase( + id="nonempty_array_gt_empty_array", + filter={"a": {"$gt": []}}, + doc=[{"_id": 1, "a": [1]}], + expected=[{"_id": 1, "a": [1]}], + msg="$gt non-empty array is greater than empty array", + ), + QueryTestCase( + id="array_with_null_element_gt_scalar", + filter={"a": {"$gt": 3}}, + doc=[{"_id": 1, "a": [None, 5]}], + expected=[{"_id": 1, "a": [None, 5]}], + msg="$gt matches array with element 5 > 3", + ), + QueryTestCase( + id="equal_array_not_gt", + filter={"a": {"$gt": [1, 2, 3]}}, + doc=[{"_id": 1, "a": [1, 2, 3]}], + expected=[], + msg="equal array [1,2,3] does not match $gt [1,2,3]", + ), + QueryTestCase( + id="empty_array_not_gt_scalar", + filter={"a": {"$gt": 5}}, + doc=[{"_id": 1, "a": []}], + expected=[], + msg="$gt 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_gt_field_lookup(collection, test): + """Parametrized test for $gt field lookup and array comparison.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_numeric_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_numeric_edge_cases.py new file mode 100644 index 00000000..a539045d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_numeric_edge_cases.py @@ -0,0 +1,306 @@ +""" +Tests for $gt numeric edge cases. + +Covers cross-type numeric comparisons (int, long, double, decimal128), +non-matching cross-type comparison, INT32/INT64 boundary values, +NaN and Decimal128 NaN, 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_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DOUBLE_MAX_SAFE_INTEGER, + DOUBLE_NEAR_MAX, + DOUBLE_NEGATIVE_ZERO, + DOUBLE_PRECISION_LOSS, + FLOAT_INFINITY, + FLOAT_NAN, + INT32_MAX, + INT32_MIN, + INT64_MAX, + INT64_MIN, +) + +CROSS_TYPE_NUMERIC_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="int_field_gt_double_query", + filter={"a": {"$gt": 4.5}}, + doc=[{"_id": 1, "a": 5}, {"_id": 2, "a": 4}], + expected=[{"_id": 1, "a": 5}], + msg="int field > double query via type bracketing", + ), + QueryTestCase( + id="double_field_gt_int_query", + filter={"a": {"$gt": 5}}, + doc=[{"_id": 1, "a": 5.5}, {"_id": 2, "a": 4.5}], + expected=[{"_id": 1, "a": 5.5}], + msg="double field > int query via type bracketing", + ), + QueryTestCase( + id="int_field_gt_long_query", + filter={"a": {"$gt": Int64(4)}}, + doc=[{"_id": 1, "a": 5}, {"_id": 2, "a": 3}], + expected=[{"_id": 1, "a": 5}], + msg="int field > long query", + ), + QueryTestCase( + id="long_field_gt_int_query", + filter={"a": {"$gt": 4}}, + doc=[{"_id": 1, "a": Int64(5)}, {"_id": 2, "a": Int64(3)}], + expected=[{"_id": 1, "a": Int64(5)}], + msg="long field > int query", + ), + QueryTestCase( + id="int_field_gt_decimal128_query", + filter={"a": {"$gt": Decimal128("4")}}, + doc=[{"_id": 1, "a": 5}, {"_id": 2, "a": 3}], + expected=[{"_id": 1, "a": 5}], + msg="int field > decimal128 query", + ), + QueryTestCase( + id="decimal128_field_gt_int_query", + filter={"a": {"$gt": 4}}, + doc=[{"_id": 1, "a": Decimal128("5")}, {"_id": 2, "a": Decimal128("3")}], + expected=[{"_id": 1, "a": Decimal128("5")}], + msg="decimal128 field > int query", + ), + QueryTestCase( + id="long_field_gt_decimal128_query", + filter={"a": {"$gt": Decimal128("4")}}, + doc=[{"_id": 1, "a": Int64(5)}, {"_id": 2, "a": Int64(3)}], + expected=[{"_id": 1, "a": Int64(5)}], + msg="long field > decimal128 query", + ), + QueryTestCase( + id="decimal128_field_gt_long_query", + filter={"a": {"$gt": Int64(4)}}, + doc=[{"_id": 1, "a": Decimal128("5")}, {"_id": 2, "a": Decimal128("3")}], + expected=[{"_id": 1, "a": Decimal128("5")}], + msg="decimal128 field > long query", + ), + QueryTestCase( + id="double_field_gt_decimal128_query", + filter={"a": {"$gt": Decimal128("4.5")}}, + doc=[{"_id": 1, "a": 5.5}, {"_id": 2, "a": 3.5}], + expected=[{"_id": 1, "a": 5.5}], + msg="double field > decimal128 query", + ), + QueryTestCase( + id="decimal128_field_gt_double_query", + filter={"a": {"$gt": 4.5}}, + doc=[{"_id": 1, "a": Decimal128("5.5")}, {"_id": 2, "a": Decimal128("3.5")}], + expected=[{"_id": 1, "a": Decimal128("5.5")}], + msg="decimal128 field > double query", + ), +] + +NON_MATCHING_CROSS_TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="string_field_not_gt_int_query", + filter={"a": {"$gt": 10}}, + doc=[{"_id": 1, "a": "hello"}], + expected=[], + msg="string field does not match $gt with int query", + ), +] + +BOUNDARY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="int64_max_equal_no_match", + filter={"a": {"$gt": INT64_MAX}}, + doc=[{"_id": 1, "a": INT64_MAX}], + expected=[], + msg="$gt with INT64_MAX equal value does not match", + ), + QueryTestCase( + id="int64_max_gt_int64_max_minus_1", + filter={"a": {"$gt": INT64_MAX - 1}}, + doc=[{"_id": 1, "a": INT64_MAX}], + expected=[{"_id": 1, "a": INT64_MAX}], + msg="$gt with INT64_MAX matches value one greater", + ), + QueryTestCase( + id="int64_min_equal_no_match", + filter={"a": {"$gt": INT64_MIN}}, + doc=[{"_id": 1, "a": INT64_MIN}], + expected=[], + msg="$gt with INT64_MIN equal value does not match", + ), + QueryTestCase( + id="int64_min_plus1_gt_int64_min", + filter={"a": {"$gt": INT64_MIN}}, + doc=[{"_id": 1, "a": Int64(int(INT64_MIN) + 1)}], + expected=[{"_id": 1, "a": Int64(int(INT64_MIN) + 1)}], + msg="INT64_MIN+1 is greater than INT64_MIN", + ), + QueryTestCase( + id="int32_max_equal_no_match", + filter={"a": {"$gt": INT32_MAX}}, + doc=[{"_id": 1, "a": INT32_MAX}], + expected=[], + msg="$gt with INT32_MAX equal value does not match", + ), + QueryTestCase( + id="int32_max_plus1_as_long_gt_int32_max", + filter={"a": {"$gt": INT32_MAX}}, + doc=[{"_id": 1, "a": Int64(INT32_MAX + 1)}], + expected=[{"_id": 1, "a": Int64(INT32_MAX + 1)}], + msg="INT32_MAX+1 (stored as long) is greater than INT32_MAX", + ), + QueryTestCase( + id="int32_min_equal_no_match", + filter={"a": {"$gt": INT32_MIN}}, + doc=[{"_id": 1, "a": INT32_MIN}], + expected=[], + msg="$gt with INT32_MIN equal value does not match", + ), + QueryTestCase( + id="int32_min_minus1_as_long_not_gt_int32_min", + filter={"a": {"$gt": INT32_MIN}}, + doc=[{"_id": 1, "a": Int64(INT32_MIN - 1)}], + expected=[], + msg="INT32_MIN-1 (stored as long) is not greater than INT32_MIN", + ), +] + +NAN_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="float_nan_field_not_gt_number", + filter={"a": {"$gt": 5}}, + doc=[{"_id": 1, "a": FLOAT_NAN}], + expected=[], + msg="NaN field does not match $gt 5", + ), + QueryTestCase( + id="number_not_gt_float_nan_query", + filter={"a": {"$gt": FLOAT_NAN}}, + doc=[{"_id": 1, "a": 5}], + expected=[], + msg="numeric field does not match $gt NaN", + ), + QueryTestCase( + id="float_nan_not_gt_float_nan", + filter={"a": {"$gt": FLOAT_NAN}}, + doc=[{"_id": 1, "a": FLOAT_NAN}], + expected=[], + msg="NaN is not greater than NaN", + ), + QueryTestCase( + id="decimal128_nan_field_not_gt_number", + filter={"a": {"$gt": 5}}, + doc=[{"_id": 1, "a": DECIMAL128_NAN}], + expected=[], + msg="Decimal128 NaN field does not match $gt 5", + ), + QueryTestCase( + id="number_not_gt_decimal128_nan_query", + filter={"a": {"$gt": DECIMAL128_NAN}}, + doc=[{"_id": 1, "a": 5}], + expected=[], + msg="numeric field does not match $gt Decimal128 NaN", + ), + QueryTestCase( + id="decimal128_nan_not_gt_decimal128_nan", + filter={"a": {"$gt": DECIMAL128_NAN}}, + doc=[{"_id": 1, "a": DECIMAL128_NAN}], + expected=[], + msg="Decimal128 NaN is not greater than Decimal128 NaN", + ), +] + +INFINITY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="infinity_field_gt_large_number", + filter={"a": {"$gt": 999999}}, + doc=[{"_id": 1, "a": FLOAT_INFINITY}], + expected=[{"_id": 1, "a": FLOAT_INFINITY}], + msg="Infinity is greater than large number", + ), + QueryTestCase( + id="number_not_gt_infinity_query", + filter={"a": {"$gt": FLOAT_INFINITY}}, + doc=[{"_id": 1, "a": -999999}], + expected=[], + msg="negative number is not greater than Infinity", + ), + QueryTestCase( + id="large_double_not_gt_infinity", + filter={"a": {"$gt": FLOAT_INFINITY}}, + doc=[{"_id": 1, "a": DOUBLE_NEAR_MAX}], + expected=[], + msg="1e308 is not greater than Infinity", + ), +] + +NEGATIVE_ZERO_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="negative_zero_not_gt_positive_zero", + filter={"a": {"$gt": 0.0}}, + doc=[{"_id": 1, "a": DOUBLE_NEGATIVE_ZERO}], + expected=[], + msg="-0.0 is not greater than 0.0 (they are equal)", + ), +] + +PRECISION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="long_2e53_plus1_gt_double_2e53", + filter={"a": {"$gt": 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 greater than double(2^53) — precision loss boundary", + ), + QueryTestCase( + id="double_rounded_up_gt_int64_max", + filter={"a": {"$gt": INT64_MAX}}, + doc=[{"_id": 1, "a": float(INT64_MAX)}], + expected=[{"_id": 1, "a": float(INT64_MAX)}], + msg="Rounded-up double representation is greater than INT64_MAX", + ), +] + +DECIMAL128_INFINITY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="decimal128_zero_not_gt_decimal128_infinity", + filter={"a": {"$gt": DECIMAL128_INFINITY}}, + doc=[{"_id": 1, "a": Decimal128("0")}], + expected=[], + msg="Decimal128 0 is not greater than Decimal128 Infinity", + ), + QueryTestCase( + id="decimal128_number_gt_decimal128_neg_infinity", + filter={"a": {"$gt": DECIMAL128_NEGATIVE_INFINITY}}, + doc=[{"_id": 1, "a": Decimal128("-999999")}], + expected=[{"_id": 1, "a": Decimal128("-999999")}], + msg="Decimal128 number is greater than Decimal128 -Infinity", + ), +] + +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 +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_gt_numeric_edge_cases(collection, test): + """Parametrized test for $gt numeric edge cases.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_value_matching.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_value_matching.py new file mode 100644 index 00000000..c682892d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/gt/test_gt_value_matching.py @@ -0,0 +1,136 @@ +""" +Tests for $gt value matching — arrays, objects, dates, and timestamps. + +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, +and Date vs Timestamp type distinction. +""" + +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": {"$gt": [1, 2, 3]}}, + doc=[{"_id": 1, "a": [3, 2, 1]}], + expected=[{"_id": 1, "a": [3, 2, 1]}], + msg="$gt [3,2,1] > [1,2,3] because first element 3 > 1", + ), + QueryTestCase( + id="nested_array_not_traversed", + filter={"a": {"$gt": 5}}, + doc=[{"_id": 1, "a": [[6, 7], [8, 9]]}], + expected=[], + msg="$gt scalar does not traverse nested arrays", + ), +] + +OBJECT_COMPARISON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="subdocument_field_value_gt", + filter={"a": {"$gt": SON([("x", 2), ("y", 1)])}}, + doc=[ + {"_id": 1, "a": SON([("x", 3), ("y", 1)])}, + {"_id": 2, "a": SON([("x", 1), ("y", 1)])}, + ], + expected=[{"_id": 1, "a": {"x": 3, "y": 1}}], + msg="$gt subdocument compares field values in order", + ), + QueryTestCase( + id="nonempty_doc_gt_empty", + filter={"a": {"$gt": {}}}, + doc=[{"_id": 1, "a": {}}, {"_id": 2, "a": {"x": 1}}], + expected=[{"_id": 2, "a": {"x": 1}}], + msg="$gt non-empty document is greater than empty document", + ), + QueryTestCase( + id="subdocument_numeric_gt_nan_subdocument", + filter={"a": {"$gt": SON([("x", float("nan"))])}}, + doc=[ + {"_id": 1, "a": SON([("x", 5)])}, + {"_id": 2, "a": SON([("x", float("nan"))])}, + ], + expected=[{"_id": 1, "a": {"x": 5}}], + msg="$gt subdocument with numeric > subdocument with NaN (NaN sorts lowest)", + ), + QueryTestCase( + id="longer_subdoc_gt_shorter_prefix", + filter={"a": {"$gt": SON([("x", 1)])}}, + doc=[ + {"_id": 1, "a": SON([("x", 1)])}, + {"_id": 2, "a": SON([("x", 1), ("y", 2)])}, + {"_id": 3, "a": SON([("x", 1), ("y", 5)])}, + ], + expected=[ + {"_id": 2, "a": {"x": 1, "y": 2}}, + {"_id": 3, "a": {"x": 1, "y": 5}}, + ], + msg="subdocument with more fields is greater than shorter prefix; equal excluded", + ), +] + +DATE_COMPARISON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="epoch_gt_pre_epoch", + filter={"a": {"$gt": DATE_BEFORE_EPOCH}}, + doc=[{"_id": 1, "a": DATE_BEFORE_EPOCH}, {"_id": 2, "a": DATE_EPOCH}], + expected=[{"_id": 2, "a": DATE_EPOCH}], + msg="epoch is greater than pre-epoch date", + ), + QueryTestCase( + id="date_equal_not_gt", + filter={"a": {"$gt": DATE_BEFORE_EPOCH}}, + doc=[{"_id": 1, "a": DATE_BEFORE_EPOCH}], + expected=[], + msg="equal date does not match $gt", + ), + QueryTestCase( + id="ts_seconds_then_increment", + filter={"a": {"$gt": Timestamp(100, 1)}}, + doc=[ + {"_id": 1, "a": Timestamp(100, 1)}, + {"_id": 2, "a": Timestamp(100, 2)}, + {"_id": 3, "a": Timestamp(99, 999)}, + ], + expected=[{"_id": 2, "a": Timestamp(100, 2)}], + msg="Timestamp orders by seconds first, then increment", + ), + QueryTestCase( + id="date_not_gt_timestamp", + filter={"a": {"$gt": Timestamp(0, 1)}}, + doc=[{"_id": 1, "a": DATE_EPOCH}], + expected=[], + msg="Date field does not match $gt with Timestamp query (different BSON types)", + ), + QueryTestCase( + id="timestamp_not_gt_date", + filter={"a": {"$gt": DATE_EPOCH}}, + doc=[{"_id": 1, "a": TS_EPOCH}], + expected=[], + msg="Timestamp field does not match $gt with Date query (different BSON types)", + ), +] + +ALL_TESTS = ARRAY_COMPARISON_TESTS + OBJECT_COMPARISON_TESTS + DATE_COMPARISON_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_gt_value_matching(collection, test): + """Parametrized test for $gt value matching.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True) From 0f93993123a02cd17e12a77a534e102b8bf8f729 Mon Sep 17 00:00:00 2001 From: Victor Tsang Date: Wed, 15 Apr 2026 17:23:01 -0700 Subject: [PATCH 2/3] added codec_options to execute_command Signed-off-by: Victor Tsang --- documentdb_tests/framework/executor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentdb_tests/framework/executor.py b/documentdb_tests/framework/executor.py index df86d0a3..b8a48331 100644 --- a/documentdb_tests/framework/executor.py +++ b/documentdb_tests/framework/executor.py @@ -5,7 +5,7 @@ from typing import Any, Dict, Union -def execute_command(collection, command: Dict) -> Union[Any, Exception]: +def execute_command(collection, command: Dict, codec_options=None) -> Union[Any, Exception]: """ Execute a DocumentDB command and return result or exception. @@ -18,7 +18,7 @@ def execute_command(collection, command: Dict) -> Union[Any, Exception]: """ try: db = collection.database - result = db.command(command) + result = db.command(command, codec_options=codec_options) return result except Exception as e: return e From d20c1ac5d9e7038e3a7355f28ff72c9a66bbc75a Mon Sep 17 00:00:00 2001 From: Victor Tsang Date: Fri, 17 Apr 2026 12:03:52 -0700 Subject: [PATCH 3/3] updated docs Signed-off-by: Victor Tsang --- docs/testing/TEST_COVERAGE.md | 1 + documentdb_tests/framework/executor.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/testing/TEST_COVERAGE.md b/docs/testing/TEST_COVERAGE.md index c515508e..499eebce 100644 --- a/docs/testing/TEST_COVERAGE.md +++ b/docs/testing/TEST_COVERAGE.md @@ -190,6 +190,7 @@ Example path: `documentdb_tests/compatibility/tests/core/operator/expressions/ar - Date + NaN (should fail) - Date + non-numeric types (should fail) - **Overflow**: Date + LONG_MAX (should fail) +- **Timezone awareness**: Use `CodecOptions(tz_aware=True, tzinfo=timezone.utc)` to verify timezone-aware datetime decoding, e.g. `datetime(2024, 1, 1, tzinfo=timezone.utc)` --- diff --git a/documentdb_tests/framework/executor.py b/documentdb_tests/framework/executor.py index b8a48331..abd73a38 100644 --- a/documentdb_tests/framework/executor.py +++ b/documentdb_tests/framework/executor.py @@ -12,6 +12,7 @@ def execute_command(collection, command: Dict, codec_options=None) -> Union[Any, Args: collection: DocumentDB collection command: Command to execute via runCommand + codec_options: Optional CodecOptions to verify timezone-aware datetime results Returns: Result if successful, Exception if failed