diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/__init__.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/__init__.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_core_functionality.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_core_functionality.py new file mode 100644 index 00000000..1aeb7072 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_core_functionality.py @@ -0,0 +1,197 @@ +""" +Tests for $polygon core geometric behavior. + +Validates valid point counts, point containment, concave polygon shapes, +winding order invariance, implicit and explicit closure, coordinate +behavior, and planar geometry. +""" + +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 + +VALID_POINT_COUNT_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="three_points_triangle", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [3, 6], [6, 0]]}}}, + doc=[{"_id": 1, "loc": [2, 2]}, {"_id": 2, "loc": [10, 10]}], + expected=[{"_id": 1, "loc": [2, 2]}], + msg="$polygon with 3 points (triangle) should succeed", + ), + QueryTestCase( + id="four_points_quadrilateral", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 5], [5, 5], [5, 0]]}}}, + doc=[{"_id": 1, "loc": [2, 2]}, {"_id": 2, "loc": [10, 10]}], + expected=[{"_id": 1, "loc": [2, 2]}], + msg="$polygon with 4 points (quadrilateral) should succeed", + ), + QueryTestCase( + id="five_points_pentagon", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [2, 5], [5, 5], [7, 2], [4, -1]]}}}, + doc=[{"_id": 1, "loc": [3, 3]}, {"_id": 2, "loc": [20, 20]}], + expected=[{"_id": 1, "loc": [3, 3]}], + msg="$polygon with 5 points (pentagon) should succeed", + ), +] + +POINT_CONTAINMENT_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="point_inside_triangle", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [10, 0], [5, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 3]}, {"_id": 2, "loc": [20, 20]}], + expected=[{"_id": 1, "loc": [5, 3]}], + msg="Point inside triangle should match", + ), + QueryTestCase( + id="point_outside_triangle", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [10, 0], [5, 10]]}}}, + doc=[{"_id": 1, "loc": [20, 20]}, {"_id": 2, "loc": [-5, -5]}], + expected=[], + msg="Points outside triangle should not match", + ), + QueryTestCase( + id="point_at_origin_inside", + filter={"loc": {"$geoWithin": {"$polygon": [[-5, -5], [5, -5], [5, 5], [-5, 5]]}}}, + doc=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [10, 10]}], + expected=[{"_id": 1, "loc": [0, 0]}], + msg="Point at origin inside polygon should match", + ), + QueryTestCase( + id="multiple_points_inside_and_outside", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [2, 8]}, + {"_id": 3, "loc": [15, 15]}, + {"_id": 4, "loc": [-1, -1]}, + ], + expected=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [2, 8]}], + msg="Only points inside polygon should match", + ), + QueryTestCase( + id="concave_excludes_concavity", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [5, 7], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": [2, 2]}, + {"_id": 2, "loc": [8, 2]}, + {"_id": 3, "loc": [5, 9]}, + ], + expected=[{"_id": 1, "loc": [2, 2]}, {"_id": 2, "loc": [8, 2]}], + msg="Concave polygon should correctly exclude points in concavity", + ), +] + +WINDING_ORDER_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="counter_clockwise_winding", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [10, 0], [10, 10], [0, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [15, 15]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="CCW winding should produce same results as CW", + ), +] + +CLOSURE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="implicit_closure", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [10, 0], [5, 10]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [15, 15]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="Implicitly closed polygon should contain point", + ), + QueryTestCase( + id="explicit_closure", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [10, 0], [5, 10], [0, 0]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [15, 15]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="Explicitly closed polygon should produce same results as implicit", + ), +] + +COORDINATE_BEHAVIOR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="negative_coordinates", + filter={"loc": {"$geoWithin": {"$polygon": [[-5, -5], [-5, 5], [5, 5], [5, -5]]}}}, + doc=[{"_id": 1, "loc": [-2, -2]}, {"_id": 2, "loc": [10, 10]}], + expected=[{"_id": 1, "loc": [-2, -2]}], + msg="Polygon with negative coordinates should work", + ), + QueryTestCase( + id="large_coordinates", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 1000], [1000, 1000], [1000, 0]]}}}, + doc=[{"_id": 1, "loc": [500, 500]}, {"_id": 2, "loc": [2000, 2000]}], + expected=[{"_id": 1, "loc": [500, 500]}], + msg="Polygon with large coordinates should work", + ), + QueryTestCase( + id="longitude_first_convention", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [10, 0], [10, 2], [0, 2]]}}}, + doc=[{"_id": 1, "loc": [5, 1]}, {"_id": 2, "loc": [1, 5]}], + expected=[{"_id": 1, "loc": [5, 1]}], + msg="$polygon should use longitude-first convention", + ), + QueryTestCase( + id="no_holes_support", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="$polygon accepts only exterior ring, no holes syntax", + ), +] + +PLANAR_GEOMETRY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="antimeridian_no_wrap", + filter={"loc": {"$geoWithin": {"$polygon": [[-179, -1], [179, -1], [179, 1], [-179, 1]]}}}, + doc=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [179, 0]}, + {"_id": 3, "loc": [-179, 0]}, + ], + expected=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [179, 0]}, + {"_id": 3, "loc": [-179, 0]}, + ], + msg="Planar geometry should not wrap at antimeridian", + ), + QueryTestCase( + id="planar_large_area", + filter={ + "loc": {"$geoWithin": {"$polygon": [[-100, -90], [100, -90], [100, 90], [-100, 90]]}} + }, + doc=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [50, 50]}, + {"_id": 3, "loc": [100, 80]}, + ], + expected=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [50, 50]}, + {"_id": 3, "loc": [100, 80]}, + ], + msg="Large polygon should use flat geometry", + ), +] + +CORE_FUNCTIONALITY_TESTS: list[QueryTestCase] = ( + VALID_POINT_COUNT_TESTS + + POINT_CONTAINMENT_TESTS + + WINDING_ORDER_TESTS + + CLOSURE_TESTS + + COORDINATE_BEHAVIOR_TESTS + + PLANAR_GEOMETRY_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(CORE_FUNCTIONALITY_TESTS)) +def test_polygon_core(collection, test): + """Test $polygon core geometric behavior.""" + 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/geospatial/specifiers/polygon/test_polygon_data_types.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_data_types.py new file mode 100644 index 00000000..ab075740 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_data_types.py @@ -0,0 +1,259 @@ +""" +Tests for $polygon data type coverage. + +Validates numeric types in coordinates, matching location field types, +non-matching location field types, and embedded object and nested field +path behavior. +""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, 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 FLOAT_INFINITY, FLOAT_NAN + +NUMERIC_COORDINATE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="double_coordinates", + filter={"loc": {"$geoWithin": {"$polygon": [[0.0, 0.0], [3.5, 6.5], [7.0, 0.0]]}}}, + doc=[{"_id": 1, "loc": [2.0, 2.0]}, {"_id": 2, "loc": [10.0, 10.0]}], + expected=[{"_id": 1, "loc": [2.0, 2.0]}], + msg="$polygon with double coordinates should succeed", + ), + QueryTestCase( + id="long_coordinates", + filter={ + "loc": { + "$geoWithin": { + "$polygon": [ + [Int64(0), Int64(0)], + [Int64(3), Int64(6)], + [Int64(6), Int64(0)], + ] + } + } + }, + doc=[{"_id": 1, "loc": [2, 2]}, {"_id": 2, "loc": [10, 10]}], + expected=[{"_id": 1, "loc": [2, 2]}], + msg="$polygon with Int64 coordinates should succeed", + ), + QueryTestCase( + id="decimal128_coordinates", + filter={ + "loc": { + "$geoWithin": { + "$polygon": [ + [Decimal128("0"), Decimal128("0")], + [Decimal128("3"), Decimal128("6")], + [Decimal128("6"), Decimal128("0")], + ] + } + } + }, + doc=[{"_id": 1, "loc": [2, 2]}, {"_id": 2, "loc": [10, 10]}], + expected=[{"_id": 1, "loc": [2, 2]}], + msg="$polygon with Decimal128 coordinates should succeed", + ), + QueryTestCase( + id="mixed_numeric_types", + filter={ + "loc": { + "$geoWithin": { + "$polygon": [ + [0, 0.0], + [Int64(3), Decimal128("6")], + [6, 0], + ] + } + } + }, + doc=[{"_id": 1, "loc": [2, 2]}, {"_id": 2, "loc": [10, 10]}], + expected=[{"_id": 1, "loc": [2, 2]}], + msg="$polygon with mixed numeric types should succeed", + ), +] + + +LOCATION_FIELD_TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="legacy_coordinate_pair", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [15, 15]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="Legacy coordinate pair should match", + ), + QueryTestCase( + id="geojson_point_also_matches", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [5, 5]}}, + {"_id": 2, "loc": [5, 5]}, + {"_id": 3, "loc": [15, 15]}, + ], + expected=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [5, 5]}}, + {"_id": 2, "loc": [5, 5]}, + ], + msg="$polygon matches both GeoJSON points and legacy coordinate pairs", + ), +] + +NON_MATCHING_FIELD_TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="null_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + 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_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "name": "no_loc"}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="Missing location field should not match", + ), + QueryTestCase( + id="string_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": "not_a_point"}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="String location field should not match", + ), + QueryTestCase( + id="int_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": 42}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="Integer location field should not match", + ), + QueryTestCase( + id="boolean_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": True}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="Boolean location field should not match", + ), + QueryTestCase( + id="nan_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": [FLOAT_NAN, FLOAT_NAN]}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="NaN location field should not match", + ), + QueryTestCase( + id="infinity_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": [FLOAT_INFINITY, FLOAT_INFINITY]}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="Infinity location field should not match", + ), + QueryTestCase( + id="javascript_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": Code("function() {}")}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="JavaScript location field should not match", + ), + QueryTestCase( + id="binary_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": Binary(b"\x00")}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="Binary location field should not match", + ), + QueryTestCase( + id="objectid_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": ObjectId()}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="ObjectId location field should not match", + ), + QueryTestCase( + id="date_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": datetime(2024, 1, 1, tzinfo=timezone.utc)}, + {"_id": 2, "loc": [5, 5]}, + ], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="Date location field should not match", + ), + QueryTestCase( + id="regex_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": Regex(".*")}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="Regex location field should not match", + ), + QueryTestCase( + id="timestamp_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": Timestamp(0, 0)}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="Timestamp location field should not match", + ), + QueryTestCase( + id="minkey_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": MinKey()}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="MinKey location field should not match", + ), + QueryTestCase( + id="maxkey_location_no_match", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[{"_id": 1, "loc": MaxKey()}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="MaxKey location field should not match", + ), +] + + +EMBEDDED_LOCATION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="embedded_object_location", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": {"x": 5, "y": 5}}, + {"_id": 2, "loc": [5, 5]}, + {"_id": 3, "loc": [15, 15]}, + ], + expected=[{"_id": 1, "loc": {"x": 5, "y": 5}}, {"_id": 2, "loc": [5, 5]}], + msg="Embedded object {x,y} format should match like coordinate pair", + ), + QueryTestCase( + id="nested_field_missing_intermediate", + filter={"address.loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "address": {"loc": [5, 5]}}, + {"_id": 2, "other": "no_address"}, + {"_id": 3, "address": None}, + ], + expected=[{"_id": 1, "address": {"loc": [5, 5]}}], + msg="Missing intermediate in nested path should not match", + ), +] + + +DATA_TYPE_TESTS: list[QueryTestCase] = ( + NUMERIC_COORDINATE_TESTS + + LOCATION_FIELD_TYPE_TESTS + + NON_MATCHING_FIELD_TYPE_TESTS + + EMBEDDED_LOCATION_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(DATA_TYPE_TESTS)) +def test_polygon_data_types(collection, test): + """Test $polygon data type coverage.""" + 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/geospatial/specifiers/polygon/test_polygon_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_edge_cases.py new file mode 100644 index 00000000..ffa0cfd7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_edge_cases.py @@ -0,0 +1,171 @@ +""" +Tests for $polygon edge cases. + +Validates degenerate polygons, boundary coordinates, boundary inclusion +(points on edges and vertices), self-intersecting polygons, duplicate +points, and large point counts. +""" + +import math + +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 + +# 100-point polygon approximating a circle of radius 50 centered at (50, 50) +_CIRCLE_POINTS = [ + [50 + 50 * math.cos(2 * math.pi * i / 100), 50 + 50 * math.sin(2 * math.pi * i / 100)] + for i in range(100) +] + +DEGENERATE_POLYGON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="all_points_identical", + filter={"loc": {"$geoWithin": {"$polygon": [[1, 1], [1, 1], [1, 1]]}}}, + doc=[{"_id": 1, "loc": [1, 1]}, {"_id": 2, "loc": [2, 2]}], + expected=[{"_id": 1, "loc": [1, 1]}], + msg="Degenerate polygon with all identical points should match point at that location", + ), + QueryTestCase( + id="collinear_points_x_axis", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [5, 0], [10, 0]]}}}, + doc=[{"_id": 1, "loc": [3, 0]}, {"_id": 2, "loc": [3, 5]}], + expected=[{"_id": 1, "loc": [3, 0]}], + msg="Collinear points along x-axis should match points on the line segment", + ), + QueryTestCase( + id="collinear_points_y_axis", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 5], [0, 10]]}}}, + doc=[{"_id": 1, "loc": [0, 3]}, {"_id": 2, "loc": [5, 3]}], + expected=[{"_id": 1, "loc": [0, 3]}], + msg="Collinear points along y-axis should match points on the line segment", + ), + QueryTestCase( + id="two_distinct_one_duplicate", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [5, 5], [0, 0]]}}}, + doc=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [5, 5]}, {"_id": 3, "loc": [10, 10]}], + expected=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [5, 5]}], + msg="Degenerate polygon with duplicate should match points on segment", + ), + QueryTestCase( + id="consecutive_duplicate_points", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 0], [5, 5], [10, 0]]}}}, + doc=[{"_id": 1, "loc": [4, 2]}, {"_id": 2, "loc": [20, 20]}], + expected=[{"_id": 1, "loc": [4, 2]}], + msg="Polygon with consecutive duplicate points should still work", + ), + QueryTestCase( + id="self_intersecting_bowtie", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [10, 10], [10, 0], [0, 10]]}}}, + doc=[ + {"_id": 1, "loc": [2, 2]}, + {"_id": 2, "loc": [8, 8]}, + {"_id": 3, "loc": [5, 5]}, + ], + expected=[{"_id": 1, "loc": [2, 2]}, {"_id": 3, "loc": [5, 5]}], + msg="Self-intersecting bowtie should match points in its triangles", + ), +] + + +BOUNDARY_COORDINATE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="coordinates_at_zero", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [1, 0], [0, 1]]}}}, + doc=[{"_id": 1, "loc": [0.2, 0.2]}, {"_id": 2, "loc": [5, 5]}], + expected=[{"_id": 1, "loc": [0.2, 0.2]}], + msg="Polygon at origin should work", + ), + QueryTestCase( + id="small_fractional_coordinates", + filter={ + "loc": { + "$geoWithin": { + "$polygon": [[0.0001, 0.0001], [0.0001, 0.001], [0.001, 0.001], [0.001, 0.0001]] + } + } + }, + doc=[{"_id": 1, "loc": [0.0005, 0.0005]}, {"_id": 2, "loc": [1, 1]}], + expected=[{"_id": 1, "loc": [0.0005, 0.0005]}], + msg="Polygon with very small fractional coordinates should work", + ), + QueryTestCase( + id="longitude_latitude_bounds", + filter={ + "loc": {"$geoWithin": {"$polygon": [[-180, -90], [180, -90], [180, 90], [-180, 90]]}} + }, + doc=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [45, 45]}], + expected=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [45, 45]}], + msg="Polygon at lon/lat bounds should contain all points within", + ), + QueryTestCase( + id="negative_zero_coordinate", + filter={ + "loc": {"$geoWithin": {"$polygon": [[-0.0, -0.0], [-0.0, 10], [10, 10], [10, -0.0]]}} + }, + doc=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [15, 15]}], + expected=[{"_id": 1, "loc": [5, 5]}], + msg="Negative zero should behave same as zero", + ), +] + + +BOUNDARY_INCLUSION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="point_on_polygon_edge", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": [5, 0]}, # midpoint of bottom edge + {"_id": 2, "loc": [5, 5]}, # inside + {"_id": 3, "loc": [15, 15]}, # outside + ], + expected=[{"_id": 1, "loc": [5, 0]}, {"_id": 2, "loc": [5, 5]}], + msg="Point on polygon edge should match", + ), + QueryTestCase( + id="point_on_polygon_vertex", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": [0, 0]}, # on vertex + {"_id": 2, "loc": [5, 5]}, # inside + {"_id": 3, "loc": [15, 15]}, # outside + ], + expected=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [5, 5]}], + msg="Point on polygon vertex should match", + ), +] + +LARGE_POINT_COUNT_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="large_point_count", + filter={"loc": {"$geoWithin": {"$polygon": _CIRCLE_POINTS}}}, + doc=[ + {"_id": 1, "loc": [50, 50]}, # center - inside + {"_id": 2, "loc": [50, 75]}, # inside + {"_id": 3, "loc": [50, 110]}, # outside + ], + expected=[{"_id": 1, "loc": [50, 50]}, {"_id": 2, "loc": [50, 75]}], + msg="Many-point polygon should work", + ), +] + + +EDGE_CASE_TESTS: list[QueryTestCase] = ( + DEGENERATE_POLYGON_TESTS + + BOUNDARY_COORDINATE_TESTS + + BOUNDARY_INCLUSION_TESTS + + LARGE_POINT_COUNT_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(EDGE_CASE_TESTS)) +def test_polygon_edge_cases(collection, test): + """Test $polygon 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/geospatial/specifiers/polygon/test_polygon_errors.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_errors.py new file mode 100644 index 00000000..5efe8ad3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_errors.py @@ -0,0 +1,237 @@ +""" +Tests for $polygon error handling. + +Validates error codes for invalid operator usage, invalid polygon specifications, +invalid argument formats, invalid point formats, invalid coordinate types, +and special numeric values in coordinates. +""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode +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 +from documentdb_tests.framework.test_constants import FLOAT_INFINITY, FLOAT_NAN + +INVALID_OPERATOR_USAGE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="polygon_without_geoWithin", + filter={"loc": {"$polygon": [[0, 0], [1, 1], [2, 0]]}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon without $geoWithin wrapper should error", + ), + QueryTestCase( + id="polygon_with_geoIntersects", + filter={"loc": {"$geoIntersects": {"$polygon": [[0, 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with $geoIntersects should error", + ), +] + + +INVALID_POLYGON_SPEC_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="empty_array", + filter={"loc": {"$geoWithin": {"$polygon": []}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with empty array should error", + ), + QueryTestCase( + id="one_point", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with 1 point should error", + ), + QueryTestCase( + id="fewer_than_3_points", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [1, 1]]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with fewer than 3 points should error", + ), + QueryTestCase( + id="null_argument", + filter={"loc": {"$geoWithin": {"$polygon": None}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with null argument should error", + ), + QueryTestCase( + id="non_array_argument", + filter={"loc": {"$geoWithin": {"$polygon": "invalid"}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with non-array argument should error", + ), +] + + +INVALID_ARGUMENT_FORMAT_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="int_arg", + filter={"loc": {"$geoWithin": {"$polygon": 123}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with integer argument should error", + ), + QueryTestCase( + id="object_arg", + filter={"loc": {"$geoWithin": {"$polygon": {}}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with object argument should error", + ), + QueryTestCase( + id="bool_arg", + filter={"loc": {"$geoWithin": {"$polygon": True}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with boolean argument should error", + ), +] + + +INVALID_POINT_FORMAT_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="flat_array_not_nested", + filter={"loc": {"$geoWithin": {"$polygon": [1, 2, 3]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with flat array instead of nested coordinate pairs should error", + ), + QueryTestCase( + id="point_as_string", + filter={"loc": {"$geoWithin": {"$polygon": ["a", "b", "c"]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with string points should error", + ), + QueryTestCase( + id="point_with_non_numeric_string", + filter={"loc": {"$geoWithin": {"$polygon": [["x", "y"], ["a", "b"], ["c", "d"]]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with string coordinate values should error", + ), + QueryTestCase( + id="point_with_non_numeric", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], ["a", "b"], [1, 1]]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with non-numeric coordinates should error", + ), + QueryTestCase( + id="point_with_null", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [None, None], [1, 1]]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with null coordinates should error", + ), + QueryTestCase( + id="point_with_boolean", + filter={"loc": {"$geoWithin": {"$polygon": [[True, False], [0, 0], [1, 1]]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with boolean coordinates should error", + ), + QueryTestCase( + id="single_coordinate_point", + filter={"loc": {"$geoWithin": {"$polygon": [[0], [1], [2]]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with single-coordinate points should error", + ), +] + + +INVALID_COORDINATE_TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="objectid_in_coordinate", + filter={"loc": {"$geoWithin": {"$polygon": [[ObjectId(), 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="ObjectId in coordinate should error", + ), + QueryTestCase( + id="regex_in_coordinate", + filter={"loc": {"$geoWithin": {"$polygon": [[Regex("x"), 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Regex in coordinate should error", + ), + QueryTestCase( + id="timestamp_in_coordinate", + filter={"loc": {"$geoWithin": {"$polygon": [[Timestamp(0, 0), 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Timestamp in coordinate should error", + ), + QueryTestCase( + id="minkey_in_coordinate", + filter={"loc": {"$geoWithin": {"$polygon": [[MinKey(), 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="MinKey in coordinate should error", + ), + QueryTestCase( + id="maxkey_in_coordinate", + filter={"loc": {"$geoWithin": {"$polygon": [[MaxKey(), 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="MaxKey in coordinate should error", + ), + QueryTestCase( + id="bindata_in_coordinate", + filter={"loc": {"$geoWithin": {"$polygon": [[Binary(b"\x00"), 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="BinData in coordinate should error", + ), + QueryTestCase( + id="javascript_in_coordinate", + filter={"loc": {"$geoWithin": {"$polygon": [[Code("x"), 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="JavaScript in coordinate should error", + ), + QueryTestCase( + id="date_in_coordinate", + filter={ + "loc": { + "$geoWithin": { + "$polygon": [[datetime(2024, 1, 1, tzinfo=timezone.utc), 0], [1, 1], [2, 0]] + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Date in coordinate should error", + ), + QueryTestCase( + id="nan_in_coordinate", + filter={"loc": {"$geoWithin": {"$polygon": [[FLOAT_NAN, 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="NaN in coordinate should error", + ), + QueryTestCase( + id="infinity_in_coordinate", + filter={"loc": {"$geoWithin": {"$polygon": [[FLOAT_INFINITY, 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Infinity in coordinate should error", + ), + QueryTestCase( + id="object_in_coordinate", + filter={"loc": {"$geoWithin": {"$polygon": [[{"a": 1}, 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Object in coordinate should error", + ), + QueryTestCase( + id="array_in_coordinate", + filter={"loc": {"$geoWithin": {"$polygon": [[[1, 2], 0], [1, 1], [2, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Array in coordinate should error", + ), +] + + +ERROR_TESTS: list[QueryTestCase] = ( + INVALID_OPERATOR_USAGE_TESTS + + INVALID_POLYGON_SPEC_TESTS + + INVALID_ARGUMENT_FORMAT_TESTS + + INVALID_POINT_FORMAT_TESTS + + INVALID_COORDINATE_TYPE_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ERROR_TESTS)) +def test_polygon_errors(collection, test): + """Test $polygon error handling.""" + collection.insert_many([{"_id": 1, "loc": [1, 1]}]) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertFailureCode(result, test.error_code) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_index_interaction.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_index_interaction.py new file mode 100644 index 00000000..c688db41 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_index_interaction.py @@ -0,0 +1,106 @@ +""" +Tests for $polygon index interaction. + +Validates behavior with and without geospatial indexes, including +dense grid queries with 2d indexes. +""" + +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command + + +def test_polygon_with_2d_index(collection): + """Test $polygon query succeeds with 2d index.""" + collection.create_index([("loc", "2d")]) + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [15, 15]}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + }, + ) + expected = [{"_id": 1, "loc": [5, 5]}] + assertSuccess(result, expected, msg="$polygon should work with 2d index") + + +def test_polygon_index_on_different_field(collection): + """Test $polygon on field without index when different field has 2d index.""" + collection.create_index([("other_loc", "2d")]) + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5], "other_loc": [1, 1]}, + {"_id": 2, "loc": [15, 15], "other_loc": [2, 2]}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + }, + ) + expected = [{"_id": 1, "loc": [5, 5], "other_loc": [1, 1]}] + assertSuccess(result, expected, msg="$polygon should work on unindexed field") + + +def test_polygon_with_2d_index_precision(collection): + """Test $polygon with 2d index returns correct results for dense grid.""" + collection.create_index([("loc", "2d")]) + # Insert a 5x5 grid of points + docs = [] + doc_id = 1 + for x in range(5): + for y in range(5): + docs.append({"_id": doc_id, "loc": [x, y]}) + doc_id += 1 + collection.insert_many(docs) + + # Square region should contain all 25 points + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "loc": { + "$geoWithin": {"$polygon": [[-0.5, -0.5], [-0.5, 4.5], [4.5, 4.5], [4.5, -0.5]]} + } + }, + }, + ) + assertSuccess( + result, + docs, + ignore_doc_order=True, + msg="Square polygon enclosing all grid points should return all docs", + ) + + +def test_polygon_dense_grid_triangle(collection): + """Test $polygon with triangle on dense grid with 2d index.""" + collection.create_index([("loc", "2d")]) + # Insert a grid with 0.5 spacing from 0 to 9.5 (20x20 = 400 points) + docs = [] + doc_id = 1 + for i in range(20): + for j in range(20): + docs.append({"_id": doc_id, "loc": [i * 0.5, j * 0.5]}) + doc_id += 1 + collection.insert_many(docs) + + # Triangle + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$polygon": [[4, 4], [6, 4], [5, 6]]}}}, + }, + ) + # Verify it returns results without error (exact count depends on boundary inclusion) + result_docs = result["cursor"]["firstBatch"] + assertSuccess(result, result_docs, msg="Triangle on dense grid should not error") diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_query_interaction.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_query_interaction.py new file mode 100644 index 00000000..d7ee0848 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_query_interaction.py @@ -0,0 +1,245 @@ +""" +Tests for $polygon query context interaction. + +Validates $polygon in find with various options (projection, sort, limit), +combined with other operators, $expr non-support, and special collection +contexts. +""" + +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 + +FIND_QUERY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="nested_field_path", + filter={"address.loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "address": {"loc": [5, 5]}}, + {"_id": 2, "address": {"loc": [15, 15]}}, + ], + expected=[{"_id": 1, "address": {"loc": [5, 5]}}], + msg="$polygon on nested field should work", + ), + QueryTestCase( + id="empty_collection", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + expected=[], + msg="$polygon on empty collection should return empty result", + ), + QueryTestCase( + id="array_location_field", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": [[5, 5], [15, 15]]}, + {"_id": 2, "loc": [[20, 20]]}, + ], + expected=[{"_id": 1, "loc": [[5, 5], [15, 15]]}], + msg="Array location field should match if any point is inside", + ), + QueryTestCase( + id="polygon_in_expr_not_evaluated", + filter={ + "$expr": { + "$eq": [ + { + "$literal": { + "loc": { + "$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]} + } + } + }, + True, + ] + } + }, + doc=[{"_id": 1, "loc": [5, 5]}], + expected=[], + msg="$polygon inside $expr should not match (not evaluated as geo query)", + ), +] + + +COMBINED_OPERATOR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="with_and", + filter={ + "$and": [ + {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + {"status": "active"}, + ] + }, + doc=[ + {"_id": 1, "loc": [5, 5], "status": "active"}, + {"_id": 2, "loc": [5, 5], "status": "inactive"}, + {"_id": 3, "loc": [15, 15], "status": "active"}, + ], + expected=[{"_id": 1, "loc": [5, 5], "status": "active"}], + msg="$polygon with $and should filter correctly", + ), + QueryTestCase( + id="with_or", + filter={ + "$or": [ + {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + {"loc": {"$geoWithin": {"$polygon": [[20, 20], [20, 30], [30, 30], [30, 20]]}}}, + ] + }, + doc=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [15, 15]}, + {"_id": 3, "loc": [25, 25]}, + ], + expected=[{"_id": 1, "loc": [5, 5]}, {"_id": 3, "loc": [25, 25]}], + msg="$polygon with $or should match either polygon", + ), + QueryTestCase( + id="with_box_via_or", + filter={ + "$or": [ + {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + {"loc": {"$geoWithin": {"$box": [[20, 20], [30, 30]]}}}, + ] + }, + doc=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [25, 25]}, + {"_id": 3, "loc": [50, 50]}, + ], + expected=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [25, 25]}], + msg="$polygon and $box via $or should both work", + ), + QueryTestCase( + id="with_center_via_or", + filter={ + "$or": [ + {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + {"loc": {"$geoWithin": {"$center": [[25, 25], 2]}}}, + ] + }, + doc=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [25, 25]}, + {"_id": 3, "loc": [50, 50]}, + ], + expected=[{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [25, 25]}], + msg="$polygon and $center via $or should both work", + ), +] + + +QUERY_INTERACTION_TESTS: list[QueryTestCase] = FIND_QUERY_TESTS + COMBINED_OPERATOR_TESTS + + +@pytest.mark.parametrize("test", pytest_params(QUERY_INTERACTION_TESTS)) +def test_polygon_query_interaction(collection, test): + """Test $polygon query interaction.""" + if test.doc: + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True) + + +def test_polygon_with_projection(collection): + """Test $polygon with field projection.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5], "name": "A", "value": 100}, + {"_id": 2, "loc": [15, 15], "name": "B", "value": 200}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + "projection": {"name": 1}, + }, + ) + expected = [{"_id": 1, "name": "A"}] + assertSuccess( + result, expected, msg="$polygon with projection should return only projected fields" + ) + + +def test_polygon_with_sort(collection): + """Test $polygon results with sort.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5], "val": 30}, + {"_id": 2, "loc": [3, 3], "val": 10}, + {"_id": 3, "loc": [7, 7], "val": 20}, + {"_id": 4, "loc": [15, 15], "val": 5}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + "sort": {"val": 1}, + }, + ) + expected = [ + {"_id": 2, "loc": [3, 3], "val": 10}, + {"_id": 3, "loc": [7, 7], "val": 20}, + {"_id": 1, "loc": [5, 5], "val": 30}, + ] + assertSuccess(result, expected, msg="$polygon with sort should return sorted results") + + +def test_polygon_with_limit(collection): + """Test $polygon results with limit.""" + collection.insert_many( + [ + {"_id": 1, "loc": [2, 2]}, + {"_id": 2, "loc": [5, 5]}, + {"_id": 3, "loc": [8, 8]}, + {"_id": 4, "loc": [15, 15]}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + "limit": 2, + }, + ) + # Should return exactly 2 documents + result_docs = result["cursor"]["firstBatch"] + assertSuccess( + result, result_docs[:2], raw_res=False, msg="$polygon with limit should limit results" + ) + + +def test_polygon_on_capped_collection(database_client): + """Test $polygon on capped collection.""" + coll_name = "test_polygon_capped" + database_client.create_collection(coll_name, capped=True, size=4096) + coll = database_client[coll_name] + try: + coll.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [15, 15]}, + ] + ) + result = execute_command( + coll, + { + "find": coll.name, + "filter": { + "loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}} + }, + }, + ) + expected = [{"_id": 1, "loc": [5, 5]}] + assertSuccess(result, expected, msg="$polygon should work on capped collection") + finally: + database_client.drop_collection(coll_name)