From 4f98df246492c087759487888b4a8b7fb76f1d00 Mon Sep 17 00:00:00 2001 From: Victor Tsang Date: Sun, 19 Apr 2026 00:09:21 -0700 Subject: [PATCH] Added query tests for $lte Signed-off-by: Victor Tsang --- .../operator/query/comparison/lte/__init__.py | 0 .../comparison/lte/test_lte_bson_wiring.py | 163 +++++++++++++++++ .../comparison/lte/test_lte_edge_cases.py | 68 +++++++ .../comparison/lte/test_lte_field_lookup.py | 147 +++++++++++++++ .../lte/test_lte_numeric_edge_cases.py | 171 ++++++++++++++++++ .../comparison/lte/test_lte_value_matching.py | 126 +++++++++++++ 6 files changed, 675 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_bson_wiring.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_edge_cases.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_field_lookup.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_numeric_edge_cases.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_value_matching.py diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/__init__.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_bson_wiring.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_bson_wiring.py new file mode 100644 index 00000000..bdd9abc3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_bson_wiring.py @@ -0,0 +1,163 @@ +""" +Tests for $lte BSON type wiring. + +A representative sample of types to confirm $lte 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": {"$lte": 7.0}}, + doc=[{"_id": 1, "a": 1.0}, {"_id": 2, "a": 7.0}, {"_id": 3, "a": 10.0}], + expected=[{"_id": 1, "a": 1.0}, {"_id": 2, "a": 7.0}], + msg="$lte with double returns docs with value <= 7.0", + ), + QueryTestCase( + id="int", + filter={"a": {"$lte": 7}}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 7}, {"_id": 3, "a": 10}], + expected=[{"_id": 1, "a": 1}, {"_id": 2, "a": 7}], + msg="$lte with int returns docs with value <= 7", + ), + QueryTestCase( + id="long", + filter={"a": {"$lte": Int64(7)}}, + doc=[ + {"_id": 1, "a": Int64(1)}, + {"_id": 2, "a": Int64(7)}, + {"_id": 3, "a": Int64(10)}, + ], + expected=[{"_id": 1, "a": Int64(1)}, {"_id": 2, "a": Int64(7)}], + msg="$lte with long returns docs with value <= 7", + ), + QueryTestCase( + id="decimal128", + filter={"a": {"$lte": Decimal128("7")}}, + doc=[ + {"_id": 1, "a": Decimal128("1")}, + {"_id": 2, "a": Decimal128("7")}, + {"_id": 3, "a": Decimal128("10")}, + ], + expected=[{"_id": 1, "a": Decimal128("1")}, {"_id": 2, "a": Decimal128("7")}], + msg="$lte with decimal128 returns docs with value <= 7", + ), + QueryTestCase( + id="string", + filter={"a": {"$lte": "cherry"}}, + doc=[ + {"_id": 1, "a": "apple"}, + {"_id": 2, "a": "banana"}, + {"_id": 3, "a": "cherry"}, + {"_id": 4, "a": "date"}, + ], + expected=[ + {"_id": 1, "a": "apple"}, + {"_id": 2, "a": "banana"}, + {"_id": 3, "a": "cherry"}, + ], + msg="$lte with string returns docs with value <= 'cherry'", + ), + QueryTestCase( + id="date", + filter={"a": {"$lte": datetime(2024, 1, 1, tzinfo=timezone.utc)}}, + doc=[ + {"_id": 1, "a": datetime(2020, 1, 1, tzinfo=timezone.utc)}, + {"_id": 2, "a": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + {"_id": 3, "a": datetime(2025, 1, 1, tzinfo=timezone.utc)}, + ], + expected=[ + {"_id": 1, "a": datetime(2020, 1, 1, tzinfo=timezone.utc)}, + {"_id": 2, "a": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + ], + msg="$lte with date returns docs with equal or earlier dates", + ), + QueryTestCase( + id="timestamp", + filter={"a": {"$lte": Timestamp(2000, 1)}}, + doc=[ + {"_id": 1, "a": Timestamp(1000, 1)}, + {"_id": 2, "a": Timestamp(2000, 1)}, + {"_id": 3, "a": Timestamp(3000, 1)}, + ], + expected=[{"_id": 1, "a": Timestamp(1000, 1)}, {"_id": 2, "a": Timestamp(2000, 1)}], + msg="$lte with timestamp returns docs with equal or smaller timestamp", + ), + QueryTestCase( + id="objectid", + filter={"a": {"$lte": ObjectId("507f1f77bcf86cd799439012")}}, + doc=[ + {"_id": 1, "a": ObjectId("507f1f77bcf86cd799439011")}, + {"_id": 2, "a": ObjectId("507f1f77bcf86cd799439012")}, + {"_id": 3, "a": ObjectId("507f1f77bcf86cd799439013")}, + ], + expected=[ + {"_id": 1, "a": ObjectId("507f1f77bcf86cd799439011")}, + {"_id": 2, "a": ObjectId("507f1f77bcf86cd799439012")}, + ], + msg="$lte with ObjectId returns docs with equal or earlier ObjectId", + ), + QueryTestCase( + id="boolean", + filter={"a": {"$lte": True}}, + doc=[{"_id": 1, "a": False}, {"_id": 2, "a": True}], + expected=[{"_id": 1, "a": False}, {"_id": 2, "a": True}], + msg="$lte with boolean true returns both true and false", + ), + QueryTestCase( + id="bindata", + filter={"a": {"$lte": 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": 1, "a": Binary(b"\x01\x02", 128)}, + {"_id": 2, "a": Binary(b"\x05\x06", 128)}, + ], + msg="$lte with BinData returns docs with equal or smaller binary", + ), + QueryTestCase( + id="minkey", + filter={"a": {"$lte": MinKey()}}, + doc=[{"_id": 1, "a": MinKey()}, {"_id": 2, "a": 1}], + expected=[{"_id": 1, "a": MinKey()}], + msg="$lte with MinKey returns only MinKey doc", + ), + QueryTestCase( + id="maxkey", + filter={"a": {"$lte": MaxKey()}}, + doc=[ + {"_id": 1, "a": 1}, + {"_id": 2, "a": "hello"}, + {"_id": 3, "a": MaxKey()}, + ], + expected=[{"_id": 1, "a": 1}, {"_id": 2, "a": "hello"}, {"_id": 3, "a": MaxKey()}], + msg="$lte with MaxKey returns all docs", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TESTS)) +def test_lte_bson_wiring(collection, test): + """Parametrized test for $lte 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/lte/test_lte_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_edge_cases.py new file mode 100644 index 00000000..23058e57 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_edge_cases.py @@ -0,0 +1,68 @@ +""" +Edge case tests for $lte operator. + +Covers deeply nested field paths with NaN, large array element matching, +empty string ordering, and null/missing field handling. +""" + +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 + +EDGE_CASE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="deeply_nested_field_with_nan", + filter={"a.b.c.d.e": {"$lte": 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": 1, "a": {"b": {"c": {"d": {"e": 5}}}}}], + msg="$lte on deeply nested field; NaN does not satisfy $lte", + ), + QueryTestCase( + id="large_array_element_match", + filter={"a": {"$lte": 0}}, + doc=[ + {"_id": 1, "a": list(range(1, 1001)) + [0]}, + {"_id": 2, "a": list(range(1, 1001))}, + ], + expected=[{"_id": 1, "a": list(range(1, 1001)) + [0]}], + msg="$lte matches element in a large (1001-element) array", + ), + QueryTestCase( + id="empty_string_lte_itself", + filter={"a": {"$lte": ""}}, + doc=[{"_id": 1, "a": ""}, {"_id": 2, "a": "a"}], + expected=[{"_id": 1, "a": ""}], + msg="empty string matches $lte for itself", + ), + QueryTestCase( + id="null_query_matches_null_and_missing", + filter={"a": {"$lte": None}}, + doc=[{"_id": 1, "a": 5}, {"_id": 2, "a": None}, {"_id": 3}], + expected=[{"_id": 2, "a": None}, {"_id": 3}], + msg="$lte null matches null and missing fields (null <= null)", + ), + QueryTestCase( + id="null_field_not_lte_numeric", + filter={"a": {"$lte": 5}}, + doc=[{"_id": 1, "a": None}], + expected=[], + msg="null field does not match $lte with numeric query", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(EDGE_CASE_TESTS)) +def test_lte_edge_cases(collection, test): + """Parametrized test for $lte 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/lte/test_lte_field_lookup.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_field_lookup.py new file mode 100644 index 00000000..efdde12f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_field_lookup.py @@ -0,0 +1,147 @@ +""" +Tests for $lte 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, empty array behavior, and embedded document dot-notation. +""" + +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": {"$lte": 3}}, + doc=[{"_id": 1, "a": [3, 7, 12]}, {"_id": 2, "a": [10, 20]}], + expected=[{"_id": 1, "a": [3, 7, 12]}], + msg="$lte matches if any array element satisfies condition (including equal)", + ), + QueryTestCase( + id="array_no_element_match", + filter={"a": {"$lte": 2}}, + doc=[{"_id": 1, "a": [10, 20]}], + expected=[], + msg="$lte on array with no element less than or equal to query", + ), + QueryTestCase( + id="array_index_zero", + filter={"arr.0": {"$lte": 10}}, + doc=[{"_id": 1, "arr": [10, 30]}, {"_id": 2, "arr": [25, 5]}], + expected=[{"_id": 1, "arr": [10, 30]}], + msg="$lte on array index 0 matches equal value", + ), + QueryTestCase( + id="numeric_key_on_object", + filter={"a.0.b": {"$lte": 3}}, + doc=[{"_id": 1, "a": {"0": {"b": 3}}}, {"_id": 2, "a": {"0": {"b": 10}}}], + expected=[{"_id": 1, "a": {"0": {"b": 3}}}], + msg="$lte with numeric key on object (not array)", + ), + QueryTestCase( + id="id_objectid", + filter={"_id": {"$lte": ObjectId("507f1f77bcf86cd799439012")}}, + doc=[ + {"_id": ObjectId("507f1f77bcf86cd799439011"), "a": 1}, + {"_id": ObjectId("507f1f77bcf86cd799439012"), "a": 2}, + {"_id": ObjectId("507f1f77bcf86cd799439013"), "a": 3}, + ], + expected=[ + {"_id": ObjectId("507f1f77bcf86cd799439011"), "a": 1}, + {"_id": ObjectId("507f1f77bcf86cd799439012"), "a": 2}, + ], + msg="$lte on _id with ObjectId includes equal", + ), + QueryTestCase( + id="array_of_embedded_docs_dot_notation", + filter={"a.b": {"$lte": 3}}, + doc=[ + {"_id": 1, "a": [{"b": 3}, {"b": 7}]}, + {"_id": 2, "a": [{"b": 10}, {"b": 20}]}, + ], + expected=[{"_id": 1, "a": [{"b": 3}, {"b": 7}]}], + msg="$lte on array of embedded docs via dot notation matches equal", + ), +] + +ARRAY_VALUE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="array_lte_array_element_comparison", + filter={"a": {"$lte": [1, 3]}}, + doc=[{"_id": 1, "a": [1, 2]}, {"_id": 2, "a": [1, 3]}], + expected=[{"_id": 1, "a": [1, 2]}, {"_id": 2, "a": [1, 3]}], + msg="$lte array includes equal and lesser arrays", + ), + QueryTestCase( + id="shorter_array_prefix_lte_longer", + filter={"a": {"$lte": [1, 2, 3]}}, + doc=[{"_id": 1, "a": [1, 2]}], + expected=[{"_id": 1, "a": [1, 2]}], + msg="$lte shorter array prefix is less than or equal to longer", + ), + QueryTestCase( + id="empty_array_lte_nonempty_array", + filter={"a": {"$lte": [1]}}, + doc=[{"_id": 1, "a": []}, {"_id": 2, "a": [1]}], + expected=[{"_id": 1, "a": []}, {"_id": 2, "a": [1]}], + msg="$lte empty array and equal array both match", + ), + QueryTestCase( + id="array_with_null_element_lte_scalar", + filter={"a": {"$lte": 5}}, + doc=[{"_id": 1, "a": [None, 5]}], + expected=[{"_id": 1, "a": [None, 5]}], + msg="$lte matches array with element 5 <= 5", + ), + QueryTestCase( + id="empty_array_not_lte_scalar", + filter={"a": {"$lte": 5}}, + doc=[{"_id": 1, "a": []}], + expected=[], + msg="$lte empty array does not match scalar query", + ), +] + +EMBEDDED_DOC_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="embedded_field_lte", + filter={"carrier.fee": {"$lte": 4}}, + doc=[ + {"_id": 1, "item": "nuts", "carrier": {"name": "Shipit", "fee": 3}}, + {"_id": 2, "item": "bolts", "carrier": {"name": "Shipit", "fee": 4}}, + {"_id": 3, "item": "washers", "carrier": {"name": "Shipit", "fee": 5}}, + ], + expected=[ + {"_id": 1, "item": "nuts", "carrier": {"name": "Shipit", "fee": 3}}, + {"_id": 2, "item": "bolts", "carrier": {"name": "Shipit", "fee": 4}}, + ], + msg="$lte on embedded field returns docs with fee <= 4", + ), + QueryTestCase( + id="embedded_field_missing_excluded", + filter={"carrier.fee": {"$lte": 10}}, + doc=[ + {"_id": 1, "carrier": {"fee": 3}}, + {"_id": 2, "item": "no carrier"}, + ], + expected=[{"_id": 1, "carrier": {"fee": 3}}], + msg="$lte on embedded field excludes docs missing the embedded path", + ), +] + +ALL_TESTS = FIELD_LOOKUP_TESTS + ARRAY_VALUE_TESTS + EMBEDDED_DOC_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_lte_field_lookup(collection, test): + """Parametrized test for $lte 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/lte/test_lte_numeric_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_numeric_edge_cases.py new file mode 100644 index 00000000..f683972c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_numeric_edge_cases.py @@ -0,0 +1,171 @@ +""" +Tests for $lte numeric edge cases. + +Covers cross-type numeric comparison, non-matching cross-type comparison, +INT64 boundary values, NaN (including self-matching and Decimal128 NaN +cross-type), infinity, negative zero, precision loss, and Decimal128 +infinity boundaries. +""" + +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, + INT64_MAX, +) + +CROSS_TYPE_NUMERIC_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="int_lte_double", + filter={"a": {"$lte": 5.0}}, + doc=[{"_id": 1, "a": 5}, {"_id": 2, "a": 6}], + expected=[{"_id": 1, "a": 5}], + msg="int field <= double query via type bracketing (equal match)", + ), + QueryTestCase( + id="double_lte_int", + filter={"a": {"$lte": 6}}, + doc=[{"_id": 1, "a": 5.5}, {"_id": 2, "a": 6.0}, {"_id": 3, "a": 6.5}], + expected=[{"_id": 1, "a": 5.5}, {"_id": 2, "a": 6.0}], + msg="double field <= int query via type bracketing (equal match)", + ), +] + +NON_MATCHING_CROSS_TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="string_not_lte_int", + filter={"a": {"$lte": 10}}, + doc=[{"_id": 1, "a": "hello"}], + expected=[], + msg="string field does not match $lte with int query", + ), +] + +BOUNDARY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="boundary_int64_max_equal", + filter={"a": {"$lte": INT64_MAX}}, + doc=[{"_id": 1, "a": INT64_MAX}], + expected=[{"_id": 1, "a": INT64_MAX}], + msg="$lte with INT64_MAX equal value matches", + ), + QueryTestCase( + id="boundary_int64_max_less", + filter={"a": {"$lte": INT64_MAX}}, + doc=[{"_id": 1, "a": Int64(INT64_MAX - 1)}], + expected=[{"_id": 1, "a": Int64(INT64_MAX - 1)}], + msg="$lte with INT64_MAX matches value one less", + ), +] + +NAN_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="nan_field_not_lte_number", + filter={"a": {"$lte": 5}}, + doc=[{"_id": 1, "a": FLOAT_NAN}], + expected=[], + msg="NaN field does not match $lte 5", + ), + QueryTestCase( + id="number_not_lte_nan_query", + filter={"a": {"$lte": FLOAT_NAN}}, + doc=[{"_id": 1, "a": 5}], + expected=[], + msg="numeric field does not match $lte NaN", + ), +] + +INFINITY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="number_lte_infinity", + filter={"a": {"$lte": FLOAT_INFINITY}}, + doc=[{"_id": 1, "a": 999999}], + expected=[{"_id": 1, "a": 999999}], + msg="large number is less than or equal to Infinity", + ), + QueryTestCase( + id="number_not_lte_neg_infinity", + filter={"a": {"$lte": FLOAT_NEGATIVE_INFINITY}}, + doc=[{"_id": 1, "a": -999999}], + expected=[], + msg="negative number is not less than or equal to -Infinity", + ), +] + +NEGATIVE_ZERO_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="neg_zero_lte_pos_zero", + filter={"a": {"$lte": 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_not_lte_double_2e53", + filter={"a": {"$lte": float(DOUBLE_MAX_SAFE_INTEGER)}}, + doc=[{"_id": 1, "a": Int64(DOUBLE_PRECISION_LOSS)}], + expected=[], + msg="Long(2^53+1) is not <= double(2^53) — precision loss boundary", + ), + QueryTestCase( + id="int64_max_lte_double_rounded_up", + filter={"a": {"$lte": float(INT64_MAX)}}, + doc=[{"_id": 1, "a": INT64_MAX}], + expected=[{"_id": 1, "a": INT64_MAX}], + msg="INT64_MAX is <= rounded-up double representation", + ), +] + +DECIMAL128_INFINITY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="decimal128_number_lte_inf", + filter={"a": {"$lte": DECIMAL128_INFINITY}}, + doc=[{"_id": 1, "a": Decimal128("0")}, {"_id": 2, "a": DECIMAL128_INFINITY}], + expected=[{"_id": 1, "a": Decimal128("0")}, {"_id": 2, "a": DECIMAL128_INFINITY}], + msg="Decimal128 0 and Infinity are both <= Decimal128 Infinity", + ), + QueryTestCase( + id="decimal128_number_not_lte_neg_inf", + filter={"a": {"$lte": DECIMAL128_NEGATIVE_INFINITY}}, + doc=[{"_id": 1, "a": Decimal128("-999999")}], + expected=[], + msg="Decimal128 number is not <= 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_lte_numeric_edge_cases(collection, test): + """Parametrized test for $lte 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/lte/test_lte_value_matching.py b/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_value_matching.py new file mode 100644 index 00000000..b54a870f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/comparison/lte/test_lte_value_matching.py @@ -0,0 +1,126 @@ +""" +Tests for $lte 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": {"$lte": [3, 2, 1]}}, + doc=[{"_id": 1, "a": [1, 2, 3]}, {"_id": 2, "a": [3, 2, 1]}], + expected=[{"_id": 1, "a": [1, 2, 3]}, {"_id": 2, "a": [3, 2, 1]}], + msg="$lte includes equal array and array with lesser first element", + ), + QueryTestCase( + id="nested_array_not_traversed", + filter={"a": {"$lte": 5}}, + doc=[{"_id": 1, "a": [[1, 2], [3, 4]]}], + expected=[], + msg="$lte scalar does not traverse nested arrays", + ), +] + +OBJECT_COMPARISON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="subdocument_field_value_lte", + filter={"a": {"$lte": SON([("x", 2), ("y", 1)])}}, + doc=[ + {"_id": 1, "a": SON([("x", 1), ("y", 1)])}, + {"_id": 2, "a": SON([("x", 2), ("y", 1)])}, + {"_id": 3, "a": SON([("x", 3), ("y", 1)])}, + ], + expected=[{"_id": 1, "a": {"x": 1, "y": 1}}, {"_id": 2, "a": {"x": 2, "y": 1}}], + msg="$lte subdocument includes equal and lesser", + ), + QueryTestCase( + id="empty_doc_lte_nonempty", + filter={"a": {"$lte": {"x": 1}}}, + doc=[{"_id": 1, "a": {}}, {"_id": 2, "a": {"x": 1}}, {"_id": 3, "a": {"x": 2}}], + expected=[{"_id": 1, "a": {}}, {"_id": 2, "a": {"x": 1}}], + msg="$lte empty document and equal document both match", + ), + QueryTestCase( + id="subdocument_nothing_lte_nan_subdocument", + filter={"a": {"$lte": SON([("x", float("nan"))])}}, + doc=[ + {"_id": 1, "a": SON([("x", 5)])}, + ], + expected=[], + msg="$lte nothing is less than or equal to subdocument with NaN (NaN sorts lowest)", + ), +] + +DATE_COMPARISON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="pre_epoch_lte_epoch", + filter={"a": {"$lte": DATE_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="$lte epoch returns both equal and earlier dates", + ), + QueryTestCase( + id="date_equal_matches_lte", + filter={"a": {"$lte": DATE_BEFORE_EPOCH}}, + doc=[{"_id": 1, "a": DATE_BEFORE_EPOCH}], + expected=[{"_id": 1, "a": DATE_BEFORE_EPOCH}], + msg="equal date matches $lte", + ), + QueryTestCase( + id="ts_seconds_then_increment", + filter={"a": {"$lte": Timestamp(100, 2)}}, + 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)}, + {"_id": 3, "a": Timestamp(99, 999)}, + ], + msg="Timestamp orders by seconds first, then increment", + ), + QueryTestCase( + id="date_not_lte_timestamp", + filter={"a": {"$lte": Timestamp(2000000000, 1)}}, + doc=[{"_id": 1, "a": DATE_EPOCH}], + expected=[], + msg="Date field does not match $lte with Timestamp query (different BSON types)", + ), + QueryTestCase( + id="timestamp_not_lte_date", + filter={"a": {"$lte": DATE_EPOCH}}, + doc=[{"_id": 1, "a": TS_EPOCH}], + expected=[], + msg="Timestamp field does not match $lte 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_lte_value_matching(collection, test): + """Parametrized test for $lte 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)