diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/__init__.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_argument_handling.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_argument_handling.py new file mode 100644 index 00000000..0bf5c8e6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_argument_handling.py @@ -0,0 +1,92 @@ +""" +Tests for $box valid argument handling. + +Validates accepted coordinate types, argument structures, +and document location type matching behavior. +""" + +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 assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +ARGUMENT_HANDLING_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="int_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [100, 100]]}}}, + doc=[{"_id": 1, "loc": [50, 50]}, {"_id": 2, "loc": [150, 150]}], + expected=[{"_id": 1, "loc": [50, 50]}], + msg="$box should accept int coordinates", + ), + QueryTestCase( + id="double_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[0.5, 0.5], [99.5, 99.5]]}}}, + doc=[{"_id": 1, "loc": [50.0, 50.0]}, {"_id": 2, "loc": [100.0, 100.0]}], + expected=[{"_id": 1, "loc": [50.0, 50.0]}], + msg="$box should accept double coordinates", + ), + QueryTestCase( + id="long_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[Int64(0), Int64(0)], [Int64(100), Int64(100)]]}}}, + doc=[{"_id": 1, "loc": [50, 50]}, {"_id": 2, "loc": [150, 150]}], + expected=[{"_id": 1, "loc": [50, 50]}], + msg="$box should accept long coordinates", + ), + QueryTestCase( + id="decimal128_coordinates", + filter={ + "loc": { + "$geoWithin": { + "$box": [ + [Decimal128("0"), Decimal128("0")], + [Decimal128("100"), Decimal128("100")], + ] + } + } + }, + doc=[{"_id": 1, "loc": [50, 50]}, {"_id": 2, "loc": [150, 150]}], + expected=[{"_id": 1, "loc": [50, 50]}], + msg="$box should accept decimal128 coordinates", + ), + QueryTestCase( + id="mixed_numeric_types", + filter={"loc": {"$geoWithin": {"$box": [[0, 0.5], [Int64(100), Decimal128("100")]]}}}, + doc=[{"_id": 1, "loc": [50, 50]}, {"_id": 2, "loc": [150, 150]}], + expected=[{"_id": 1, "loc": [50, 50]}], + msg="$box should accept mixed numeric types", + ), + QueryTestCase( + id="three_points_accepted", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [50, 50], [100, 100]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [150, 150]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="Three points should be accepted; only the first two define the box", + ), + QueryTestCase( + id="legacy_pair_matches", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="Legacy coordinate pair should match", + ), + QueryTestCase( + id="object_with_xy_matches", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": {"x": 5, "y": 5}}], + expected=[{"_id": 1, "loc": {"x": 5, "y": 5}}], + msg="Object with x/y fields is treated as legacy coordinate pair", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ARGUMENT_HANDLING_TESTS)) +def test_box_argument_handling(collection, test): + """Test $box argument handling — see per-case msg for details.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult(result, expected=test.expected, ignore_doc_order=True, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_bson_comparison.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_bson_comparison.py new file mode 100644 index 00000000..62eeb8ed --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_bson_comparison.py @@ -0,0 +1,68 @@ +""" +Tests for $box BSON type comparison. + +Verifies that $box coordinate values reject invalid BSON types with expected +error codes and accept valid numeric BSON types without error. +""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccess +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR +from documentdb_tests.framework.executor import execute_command + +BOX_COORDINATE_PARAMS = [ + BsonTypeTestCase( + id="box_coordinate", + msg="$box coordinates should reject non-numeric types", + keyword="$box", + valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL], + default_error_code=BAD_VALUE_ERROR, + valid_inputs={ + BsonType.DOUBLE: 10.5, + BsonType.INT: 10, + BsonType.LONG: Int64(10), + BsonType.DECIMAL: Decimal128("10"), + }, + ), +] + +REJECTION_CASES = generate_bson_rejection_test_cases(BOX_COORDINATE_PARAMS) + + +@pytest.mark.parametrize( + "bson_type,sample_value,spec", + REJECTION_CASES, +) +def test_box_bson_type_rejected(collection, bson_type, sample_value, spec): + """Test $box rejects invalid BSON types as coordinate values.""" + query = {"loc": {"$geoWithin": {"$box": [[sample_value, sample_value], [10, 10]]}}} + result = execute_command(collection, {"find": collection.name, "filter": query}) + assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) + + +ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(BOX_COORDINATE_PARAMS) + + +@pytest.mark.parametrize( + "bson_type,sample_value,spec", + ACCEPTANCE_CASES, +) +def test_box_bson_type_accepted(collection, bson_type, sample_value, spec): + """Test $box accepts valid numeric BSON types as coordinate values.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [-5, -5]}, + ] + ) + query = {"loc": {"$geoWithin": {"$box": [[0, 0], [sample_value, sample_value]]}}} + result = execute_command(collection, {"find": collection.name, "filter": query}) + assertSuccess(result, [{"_id": 1, "loc": [5, 5]}], ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_core_functionality.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_core_functionality.py new file mode 100644 index 00000000..37172d69 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_core_functionality.py @@ -0,0 +1,135 @@ +""" +Tests for $box core functionality. + +Validates basic $box behavior with $geoWithin, GeoJSON exclusion, +multiple documents, coordinate ordering, and result correctness. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +CORE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="basic_box_returns_points_inside", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [15, 15]}, + ], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="$box should return documents within rectangle bounds", + ), + QueryTestCase( + id="does_not_return_outside", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[ + {"_id": 1, "loc": [11, 11]}, + {"_id": 2, "loc": [-1, -1]}, + ], + expected=[], + msg="$box should NOT return documents outside the rectangle", + ), + QueryTestCase( + id="longitude_first_latitude_second", + filter={"loc": {"$geoWithin": {"$box": [[-74, 40], [-73, 41]]}}}, + doc=[ + {"_id": 1, "loc": [-73.5, 40.5]}, + {"_id": 2, "loc": [0, 0]}, + ], + expected=[{"_id": 1, "loc": [-73.5, 40.5]}], + msg="$box uses longitude first, latitude second ordering", + ), + QueryTestCase( + id="arbitrary_grid_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[100, 200], [300, 400]]}}}, + doc=[ + {"_id": 1, "loc": [150, 250]}, + {"_id": 2, "loc": [50, 50]}, + ], + expected=[{"_id": 1, "loc": [150, 250]}], + msg="$box works with arbitrary grid coordinates", + ), + QueryTestCase( + id="grid_count_4x4", + filter={"loc": {"$geoWithin": {"$box": [[2, 2], [5, 5]]}}}, + doc=[{"_id": i * 10 + j, "loc": [i, j]} for i in range(10) for j in range(10)], + expected=[{"_id": i * 10 + j, "loc": [i, j]} for i in range(2, 6) for j in range(2, 6)], + msg="Grid count should be 16", + ), + QueryTestCase( + id="full_coverage", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [9, 9]]}}}, + doc=[{"_id": i * 10 + j, "loc": [i, j]} for i in range(10) for j in range(10)], + expected=[{"_id": i * 10 + j, "loc": [i, j]} for i in range(10) for j in range(10)], + msg="Should return all 100 documents", + ), + QueryTestCase( + id="geojson_point_within_bounds_matched", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": {"type": "Point", "coordinates": [5, 5]}}], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [5, 5]}}], + msg="$box matches documents stored as GeoJSON Point within bounds", + ), + QueryTestCase( + id="multiple_inside_all_returned", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[ + {"_id": 1, "loc": [1, 1]}, + {"_id": 2, "loc": [5, 5]}, + {"_id": 3, "loc": [9, 9]}, + ], + expected=[ + {"_id": 1, "loc": [1, 1]}, + {"_id": 2, "loc": [5, 5]}, + {"_id": 3, "loc": [9, 9]}, + ], + msg="$box should return all documents inside the rectangle", + ), + QueryTestCase( + id="mixed_inside_outside", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [15, 15]}, + {"_id": 3, "loc": [8, 8]}, + ], + expected=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 3, "loc": [8, 8]}, + ], + msg="$box should return only documents inside the rectangle", + ), + QueryTestCase( + id="no_documents_inside", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[ + {"_id": 1, "loc": [20, 20]}, + {"_id": 2, "loc": [30, 30]}, + ], + expected=[], + msg="$box should return empty result when no documents are inside", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(CORE_TESTS)) +def test_box_core(collection, test): + """Test $box core functionality with $geoWithin.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult(result, expected=test.expected, ignore_doc_order=True, msg=test.msg) + + +def test_box_empty_collection(collection): + """Test $box returns empty result on empty collection.""" + result = execute_command( + collection, + {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}}, + ) + assertResult(result, expected=[], msg="$box should return empty result on empty collection") diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_edge_cases.py new file mode 100644 index 00000000..8514c718 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_edge_cases.py @@ -0,0 +1,256 @@ +""" +Tests for $box edge cases. + +Validates boundary coordinates, coordinate inversion, negative zero, +null/missing fields, boundary inclusion, and floating-point precision. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +EDGE_CASE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="zero_area_same_point", + filter={"loc": {"$geoWithin": {"$box": [[5, 5], [5, 5]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [6, 6]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="Zero area box (same point) should match exact point", + ), + QueryTestCase( + id="zero_width", + filter={"loc": {"$geoWithin": {"$box": [[5, 0], [5, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [6, 5]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="Zero width box should match points on the line", + ), + QueryTestCase( + id="zero_height", + filter={"loc": {"$geoWithin": {"$box": [[0, 5], [10, 5]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [5, 6]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="Zero height box should match points on the line", + ), + QueryTestCase( + id="negative_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[-100, -100], [100, 100]]}}}, + doc=[{"_id": 1, "loc": [-50, -50]}, {"_id": 2, "loc": [200, 200]}], + expected=[{"_id": 1, "loc": [-50, -50]}], + msg="$box should work with negative coordinates", + ), + QueryTestCase( + id="very_large_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [1000000, 1000000]]}}}, + doc=[{"_id": 1, "loc": [500000, 500000]}, {"_id": 2, "loc": [2000000, 2000000]}], + expected=[{"_id": 1, "loc": [500000, 500000]}], + msg="$box should work with very large coordinates", + ), + QueryTestCase( + id="very_small_area", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [0.0001, 0.0001]]}}}, + doc=[{"_id": 1, "loc": [0.00005, 0.00005]}, {"_id": 2, "loc": [1, 1]}], + expected=[{"_id": 1, "loc": [0.00005, 0.00005]}], + msg="$box should work with very small area", + ), + QueryTestCase( + id="fully_inverted", + filter={"loc": {"$geoWithin": {"$box": [[10, 10], [0, 0]]}}}, + doc=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [0, 0]}, + {"_id": 3, "loc": [10, 10]}, + ], + expected=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [0, 0]}, + {"_id": 3, "loc": [10, 10]}, + ], + msg="Inverted box returns same results as normal order", + ), + QueryTestCase( + id="x_inverted", + filter={"loc": {"$geoWithin": {"$box": [[10, 0], [0, 10]]}}}, + doc=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [0, 0]}, + {"_id": 3, "loc": [10, 10]}, + ], + expected=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [0, 0]}, + {"_id": 3, "loc": [10, 10]}, + ], + msg="X-inverted box returns same results as normal order", + ), + QueryTestCase( + id="y_inverted", + filter={"loc": {"$geoWithin": {"$box": [[0, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [0, 0]}, + {"_id": 3, "loc": [10, 10]}, + ], + expected=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [0, 0]}, + {"_id": 3, "loc": [10, 10]}, + ], + msg="Y-inverted box returns same results as normal order", + ), + QueryTestCase( + id="negative_zero_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[-0.0, -0.0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [15, 15]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="Negative zero coordinates should work normally", + ), + QueryTestCase( + id="string_field_no_match", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": "not a coordinate"}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="String location field should not match", + ), + QueryTestCase( + id="null_field", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": None}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="Null location field should not match", + ), + QueryTestCase( + id="missing_field", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "other": "value"}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="Missing location field should not match", + ), + QueryTestCase( + id="bottom_left_corner_included", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [0, 0]}], + expected=[{"_id": 1, "loc": [0, 0]}], + msg="Bottom-left corner should be included", + ), + QueryTestCase( + id="upper_right_corner_included", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [10, 10]}], + expected=[{"_id": 1, "loc": [10, 10]}], + msg="Upper-right corner should be included", + ), + QueryTestCase( + id="left_edge_included", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [0, 5]}], + expected=[{"_id": 1, "loc": [0, 5]}], + msg="Left edge should be included", + ), + QueryTestCase( + id="right_edge_included", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [10, 5]}], + expected=[{"_id": 1, "loc": [10, 5]}], + msg="Right edge should be included", + ), + QueryTestCase( + id="top_edge_included", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 10]}], + expected=[{"_id": 1, "loc": [5, 10]}], + msg="Top edge should be included", + ), + QueryTestCase( + id="bottom_edge_included", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 0]}], + expected=[{"_id": 1, "loc": [5, 0]}], + msg="Bottom edge should be included", + ), + QueryTestCase( + id="just_outside_excluded", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[ + {"_id": 1, "loc": [-0.001, 5]}, + {"_id": 2, "loc": [10.001, 5]}, + {"_id": 3, "loc": [5, 10.001]}, + {"_id": 4, "loc": [5, -0.001]}, + ], + expected=[], + msg="Points just outside each edge should not match", + ), + QueryTestCase( + id="just_inside_included", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [0.001, 0.001]}], + expected=[{"_id": 1, "loc": [0.001, 0.001]}], + msg="Point just inside should match", + ), + QueryTestCase( + id="double_near_max_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [1e308, 1e308]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="$box with DOUBLE_NEAR_MAX coordinates should work", + ), + QueryTestCase( + id="double_min_subnormal_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [5e-324, 5e-324]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}], + expected=[], + msg="$box with DOUBLE_MIN_SUBNORMAL area should not match distant points", + ), + QueryTestCase( + id="coordinates_differ_by_1e_minus_10", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [0.0000000001, 0.0000000001]]}}}, + doc=[{"_id": 1, "loc": [0.00000000005, 0.00000000005]}], + expected=[{"_id": 1, "loc": [0.00000000005, 0.00000000005]}], + msg="$box with coordinates differing by 1e-10 should match point within", + ), + QueryTestCase( + id="doc_with_3_element_array", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 5, 99]}, {"_id": 2, "loc": [15, 15, 1]}], + expected=[{"_id": 1, "loc": [5, 5, 99]}], + msg="Doc with 3-element array should match using first 2 elements", + ), + QueryTestCase( + id="linestring_not_matched", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[ + {"_id": 1, "loc": {"type": "LineString", "coordinates": [[1, 1], [5, 5]]}}, + {"_id": 2, "loc": [5, 5]}, + ], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="$box should not match LineString geometry documents", + ), + QueryTestCase( + id="polygon_not_matched", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[0, 0], [5, 0], [5, 5], [0, 5], [0, 0]]], + }, + }, + {"_id": 2, "loc": [5, 5]}, + ], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="$box should not match Polygon geometry documents", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(EDGE_CASE_TESTS)) +def test_box_edge_cases(collection, test): + """Test $box edge cases.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult(result, expected=test.expected, ignore_doc_order=True, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_errors.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_errors.py new file mode 100644 index 00000000..78a78e7e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_errors.py @@ -0,0 +1,159 @@ +""" +Tests for $box error conditions. + +Validates errors for invalid contexts, invalid argument structures, +invalid point formats, and special numeric values. + +Note: BSON type rejection/acceptance is covered in test_box_bson_comparison.py. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +ERROR_TESTS: list[QueryTestCase] = [ + # Invalid context + QueryTestCase( + id="box_without_geoWithin", + filter={"loc": {"$box": [[0, 0], [10, 10]]}}, + error_code=BAD_VALUE_ERROR, + msg="$box without $geoWithin wrapper should error", + ), + QueryTestCase( + id="box_with_geoIntersects", + filter={"loc": {"$geoIntersects": {"$box": [[0, 0], [10, 10]]}}}, + error_code=BAD_VALUE_ERROR, + msg="$box with $geoIntersects should error", + ), + QueryTestCase( + id="box_with_near", + filter={"loc": {"$near": {"$box": [[0, 0], [10, 10]]}}}, + error_code=BAD_VALUE_ERROR, + msg="$box with $near should error", + ), + # Invalid argument structure + QueryTestCase( + id="empty_array", + filter={"loc": {"$geoWithin": {"$box": []}}}, + error_code=BAD_VALUE_ERROR, + msg="Empty array should error", + ), + QueryTestCase( + id="single_point", + filter={"loc": {"$geoWithin": {"$box": [[0, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Single point should error", + ), + QueryTestCase( + id="string_argument", + filter={"loc": {"$geoWithin": {"$box": "invalid"}}}, + error_code=BAD_VALUE_ERROR, + msg="String argument should error", + ), + QueryTestCase( + id="int_argument", + filter={"loc": {"$geoWithin": {"$box": 123}}}, + error_code=BAD_VALUE_ERROR, + msg="Integer argument should error", + ), + QueryTestCase( + id="null_argument", + filter={"loc": {"$geoWithin": {"$box": None}}}, + error_code=BAD_VALUE_ERROR, + msg="Null argument should error", + ), + # Invalid point formats + QueryTestCase( + id="point_with_1_coordinate", + filter={"loc": {"$geoWithin": {"$box": [[0], [100]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Point with 1 coordinate should error", + ), + QueryTestCase( + id="point_with_3_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[0, 0, 0], [100, 100, 100]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Point with 3 coordinates should error", + ), + QueryTestCase( + id="empty_point_arrays", + filter={"loc": {"$geoWithin": {"$box": [[], []]}}}, + error_code=BAD_VALUE_ERROR, + msg="Empty point arrays should error", + ), + QueryTestCase( + id="point_as_non_array", + filter={"loc": {"$geoWithin": {"$box": [0, [100, 100]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Point as non-array should error", + ), + # Special numeric values + QueryTestCase( + id="nan_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[float("nan"), float("nan")], [10, 10]]}}}, + error_code=BAD_VALUE_ERROR, + msg="NaN coordinates in first point (bottomLeft) should error", + ), + QueryTestCase( + id="nan_coordinates_second_point", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [float("nan"), float("nan")]]}}}, + error_code=BAD_VALUE_ERROR, + msg="NaN coordinates in second point (topRight) should error", + ), + QueryTestCase( + id="infinity_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [float("inf"), float("inf")]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Infinity coordinates in second point (topRight) should error", + ), + QueryTestCase( + id="infinity_coordinates_first_point", + filter={"loc": {"$geoWithin": {"$box": [[float("inf"), float("inf")], [10, 10]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Infinity coordinates in first point (bottomLeft) should error", + ), + QueryTestCase( + id="negative_infinity_coordinates", + filter={"loc": {"$geoWithin": {"$box": [[float("-inf"), float("-inf")], [10, 10]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Negative infinity coordinates in first point (bottomLeft) should error", + ), + QueryTestCase( + id="negative_infinity_coordinates_second_point", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [float("-inf"), float("-inf")]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Negative infinity coordinates in second point (topRight) should error", + ), + # Mixed valid/invalid + QueryTestCase( + id="mixed_valid_invalid_in_point", + filter={"loc": {"$geoWithin": {"$box": [[0, "bad"], [10, 10]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Mixed valid and invalid coordinate in same point should error", + ), + QueryTestCase( + id="partial_null_in_point", + filter={"loc": {"$geoWithin": {"$box": [[0, None], [10, 10]]}}}, + error_code=BAD_VALUE_ERROR, + msg="One valid coordinate and one null in same point should error", + ), + QueryTestCase( + id="second_point_as_non_array", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], "bad"]}}}, + error_code=BAD_VALUE_ERROR, + msg="Second point as non-array should error", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ERROR_TESTS)) +def test_box_errors(collection, test): + """Test $box error conditions.""" + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult(result, error_code=test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_index_interaction.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_index_interaction.py new file mode 100644 index 00000000..0487b8d9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_index_interaction.py @@ -0,0 +1,209 @@ +""" +Tests for $box index interaction. + +Validates $box behavior with 2d index, without index, with 2dsphere index, +custom index bounds, and queries outside index bounds. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +NO_INDEX_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="without_index", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [15, 15]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="$box should work without index", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(NO_INDEX_TESTS)) +def test_box_no_index(collection, test): + """Test $box works without any geospatial index.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult(result, expected=test.expected, ignore_doc_order=True, msg=test.msg) + + +WITH_2D_INDEX_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="basic_2d_index", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [15, 15]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="$box with 2d index should return correct results", + ), + QueryTestCase( + id="query_outside_default_bounds", + filter={"loc": {"$geoWithin": {"$box": [[-200, -200], [200, 200]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="$box query outside default bounds should still return items within bounds", + ), + QueryTestCase( + id="index_bounds_clipping", + filter={"loc": {"$geoWithin": {"$box": [[-200, -200], [200, 200]]}}}, + doc=[{"_id": i, "loc": [i * 10 - 50, 0]} for i in range(11)], + expected=[{"_id": i, "loc": [i * 10 - 50, 0]} for i in range(11)], + msg="$box larger than index bounds should clip and return all items", + ), + QueryTestCase( + id="single_dimension_off_bounds", + filter={"loc": {"$geoWithin": {"$box": [[-200, -10], [200, 10]]}}}, + doc=[{"_id": i, "loc": [i, 0]} for i in range(11)], + expected=[{"_id": i, "loc": [i, 0]} for i in range(11)], + msg="$box with single dimension off-bounds should return all items within", + ), + QueryTestCase( + id="close_to_index_bounds", + filter={"loc": {"$geoWithin": {"$box": [[-179, -179], [179, 179]]}}}, + doc=[{"_id": i, "loc": [i, 0]} for i in range(11)], + expected=[{"_id": i, "loc": [i, 0]} for i in range(11)], + msg="$box close to index bounds should return all items", + ), + QueryTestCase( + id="inverted_coordinates_with_2d_index", + filter={"loc": {"$geoWithin": {"$box": [[179, 179], [-179, -179]]}}}, + doc=[{"_id": i, "loc": [i, 0]} for i in range(11)], + expected=[{"_id": i, "loc": [i, 0]} for i in range(11)], + msg="$box with inverted coordinates should return same results with 2d index", + ), + QueryTestCase( + id="mixed_inverted_coordinates_with_2d_index", + filter={"loc": {"$geoWithin": {"$box": [[179, -179], [-179, 179]]}}}, + doc=[{"_id": i, "loc": [i, 0]} for i in range(11)], + expected=[{"_id": i, "loc": [i, 0]} for i in range(11)], + msg="$box with mixed inverted coordinates should return same results with 2d index", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(WITH_2D_INDEX_TESTS)) +def test_box_with_2d_index(collection, test): + """Test $box with 2d index.""" + collection.insert_many(test.doc) + collection.create_index([("loc", "2d")]) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult(result, expected=test.expected, ignore_doc_order=True, msg=test.msg) + + +WITH_2DSPHERE_INDEX_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="basic_2dsphere_index", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [15, 15]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="$box with 2dsphere index should still work", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(WITH_2DSPHERE_INDEX_TESTS)) +def test_box_with_2dsphere_index(collection, test): + """Test $box with 2dsphere index.""" + collection.insert_many(test.doc) + collection.create_index([("loc", "2dsphere")]) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult(result, expected=test.expected, ignore_doc_order=True, msg=test.msg) + + +def test_box_custom_2d_bounds_1000(collection): + """Test $box with 2d index bounds [-1000, 1000].""" + collection.insert_many([{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [500, 500]}]) + collection.create_index([("loc", "2d")], min=-1000, max=1000) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$box": [[0, 0], [600, 600]]}}}, + }, + ) + assertResult( + result, + expected=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [500, 500]}], + ignore_doc_order=True, + msg="$box with custom 2d index bounds [-1000, 1000] should return points within box", + ) + + +def test_box_custom_2d_bounds_10m(collection): + """Test $box with 2d index bounds [-10M, 10M].""" + collection.insert_many([{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [500, 500]}]) + collection.create_index([("loc", "2d")], min=-10000000, max=10000000) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$box": [[0, 0], [600, 600]]}}}, + }, + ) + assertResult( + result, + expected=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [500, 500]}], + ignore_doc_order=True, + msg="$box with custom 2d index bounds [-10M, 10M] should return correct results", + ) + + +def test_box_custom_2d_bounds_1b(collection): + """Test $box with 2d index bounds [-1B, 1B].""" + collection.insert_many([{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [500, 500]}]) + collection.create_index([("loc", "2d")], min=-1000000000, max=1000000000) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$box": [[0, 0], [600, 600]]}}}, + }, + ) + assertResult( + result, + expected=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [500, 500]}], + ignore_doc_order=True, + msg="$box with custom 2d index bounds [-1B, 1B] should return correct results", + ) + + +def test_box_non_default_bits_precision(collection): + """Test $box with 2d index using non-default bits precision.""" + collection.insert_many([{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [15, 15]}]) + collection.create_index([("loc", "2d")], bits=16) + result = execute_command( + collection, + {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}}, + ) + assertResult( + result, + expected=[{"_id": 1, "loc": [5, 5]}], + ignore_doc_order=True, + msg="$box with non-default bits precision should return correct results", + ) + + +def test_box_with_compound_2d_index(collection): + """Test $box with compound 2d index.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5], "type": "a"}, + {"_id": 2, "loc": [15, 15], "type": "b"}, + ] + ) + collection.create_index([("loc", "2d"), ("type", 1)]) + result = execute_command( + collection, + {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}}, + ) + assertResult( + result, + expected=[{"_id": 1, "loc": [5, 5], "type": "a"}], + ignore_doc_order=True, + msg="$box with compound 2d index should return correct results", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_query_interaction.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_query_interaction.py new file mode 100644 index 00000000..f684a008 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/box/test_box_query_interaction.py @@ -0,0 +1,199 @@ +""" +Tests for $box interaction with other query operators. + +Validates $box combined with $and, $or, projection, skip, sort, +nested fields, and array fields. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +QUERY_INTERACTION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="combined_with_field_equality", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}, "type": "a"}, + doc=[ + {"_id": 1, "loc": [5, 5], "type": "a"}, + {"_id": 2, "loc": [5, 5], "type": "b"}, + {"_id": 3, "loc": [15, 15], "type": "a"}, + ], + expected=[{"_id": 1, "loc": [5, 5], "type": "a"}], + msg="$box combined with field equality should filter both", + ), + QueryTestCase( + id="combined_with_or", + filter={ + "$or": [ + {"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + {"type": "a"}, + ] + }, + doc=[ + {"_id": 1, "loc": [5, 5], "type": "a"}, + {"_id": 2, "loc": [5, 5], "type": "b"}, + {"_id": 3, "loc": [15, 15], "type": "a"}, + ], + expected=[ + {"_id": 1, "loc": [5, 5], "type": "a"}, + {"_id": 2, "loc": [5, 5], "type": "b"}, + {"_id": 3, "loc": [15, 15], "type": "a"}, + ], + msg="$box combined with $or should union results", + ), + QueryTestCase( + id="on_nested_field", + filter={"address.loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[ + {"_id": 1, "address": {"loc": [5, 5]}}, + {"_id": 2, "address": {"loc": [15, 15]}}, + ], + expected=[{"_id": 1, "address": {"loc": [5, 5]}}], + msg="$box should work on nested field path", + ), + QueryTestCase( + id="on_array_of_points", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[ + {"_id": 1, "loc": [[5, 5], [15, 15]]}, + {"_id": 2, "loc": [[20, 20], [25, 25]]}, + ], + expected=[{"_id": 1, "loc": [[5, 5], [15, 15]]}], + msg="$box on array of points should match if ANY point is within", + ), + QueryTestCase( + id="on_array_of_points_one_on_boundary", + filter={"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + doc=[ + {"_id": 1, "loc": [[10, 10], [15, 15]]}, + {"_id": 2, "loc": [[11, 11], [20, 20]]}, + ], + expected=[{"_id": 1, "loc": [[10, 10], [15, 15]]}], + msg="$box on array of points should match if one point is on boundary", + ), + QueryTestCase( + id="two_geowithin_same_field", + filter={ + "$and": [ + {"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + {"loc": {"$geoWithin": {"$box": [[5, 5], [15, 15]]}}}, + ] + }, + doc=[ + {"_id": 1, "loc": [7, 7]}, + {"_id": 2, "loc": [3, 3]}, + {"_id": 3, "loc": [12, 12]}, + ], + expected=[{"_id": 1, "loc": [7, 7]}], + msg="Two $geoWithin $box on same field via $and should intersect results", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(QUERY_INTERACTION_TESTS)) +def test_box_query_interaction(collection, test): + """Test $box combined with other query operators.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertResult(result, expected=test.expected, ignore_doc_order=True, msg=test.msg) + + +def test_box_with_projection(collection): + """Test $box with projection returns only specified fields.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5], "type": "a"}, + {"_id": 2, "loc": [5, 5], "type": "b"}, + {"_id": 3, "loc": [15, 15], "type": "a"}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + "projection": {"_id": 1, "type": 1}, + }, + ) + assertResult( + result, + expected=[{"_id": 1, "type": "a"}, {"_id": 2, "type": "b"}], + ignore_doc_order=True, + msg="$box with projection should return only projected fields", + ) + + +def test_box_with_skip(collection): + """Test $box with skip skips the first N results.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [6, 6]}, + {"_id": 3, "loc": [15, 15]}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + "sort": {"_id": 1}, + "skip": 1, + }, + ) + assertResult( + result, + expected=[{"_id": 2, "loc": [6, 6]}], + msg="$box with skip should skip first result", + ) + + +def test_box_with_sort(collection): + """Test $box with sort on a non-geo field.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5], "val": 10}, + {"_id": 2, "loc": [6, 6], "val": 20}, + {"_id": 3, "loc": [15, 15], "val": 30}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$box": [[0, 0], [10, 10]]}}}, + "sort": {"val": -1}, + }, + ) + assertResult( + result, + expected=[ + {"_id": 2, "loc": [6, 6], "val": 20}, + {"_id": 1, "loc": [5, 5], "val": 10}, + ], + msg="$box with sort should order by specified field", + ) + + +def test_box_with_limit(collection): + """Test $box query with limit returns limited number of results.""" + collection.insert_many([{"_id": i, "loc": [i, i]} for i in range(20)]) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$box": [[0, 0], [19, 19]]}}}, + "limit": 5, + "sort": {"_id": 1}, + }, + ) + assertResult( + result, + expected=[{"_id": i, "loc": [i, i]} for i in range(5)], + msg="$box with limit should return exactly 5 documents", + )