From 96a85b6e5c4b10c27d9674ce18101b900b04670f Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Thu, 14 May 2026 16:37:20 -0700 Subject: [PATCH 1/5] Initial polygon tests Signed-off-by: PatersonProjects --- .../test_polygon_core_functionality.py | 241 ++++++++++ .../polygon/test_polygon_data_types.py | 194 ++++++++ .../polygon/test_polygon_edge_cases.py | 149 ++++++ .../specifiers/polygon/test_polygon_errors.py | 314 ++++++++++++ .../polygon/test_polygon_index_interaction.py | 132 +++++ .../polygon/test_polygon_js_specs.py | 144 ++++++ .../polygon/test_polygon_query_interaction.py | 452 ++++++++++++++++++ 7 files changed, 1626 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_core_functionality.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_data_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_edge_cases.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_index_interaction.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_js_specs.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_query_interaction.py 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..fc9004eb --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_core_functionality.py @@ -0,0 +1,241 @@ +""" +Tests for $polygon core geometric behavior. + +Validates valid point counts, point containment, concave polygon shapes, +winding order invariance, implicit closure, and coordinate conventions. +""" + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(VALID_POINT_COUNT_TESTS)) +def test_polygon_valid_point_counts(collection, test): + """Test $polygon succeeds with 3 or more points.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected) + + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(POINT_CONTAINMENT_TESTS)) +def test_polygon_point_containment(collection, test): + """Test $polygon correctly identifies points inside/outside polygon.""" + 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_winding_order_invariance(collection): + """Test $polygon produces same results regardless of winding order.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [15, 15]}, + ] + ) + # Clockwise + result_cw = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [10, 0], [10, 10], [0, 10]]}}}, + }, + ) + # Counter-clockwise (verify it also succeeds) + 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_cw, expected, msg="Clockwise winding should match point inside") + + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(CLOSURE_TESTS)) +def test_polygon_closure(collection, test): + """Test $polygon implicit and explicit polygon closure.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected) + + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(COORDINATE_BEHAVIOR_TESTS)) +def test_polygon_coordinate_behavior(collection, test): + """Test $polygon with various coordinate ranges and conventions.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected) + + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(PLANAR_GEOMETRY_TESTS)) +def test_polygon_planar_geometry(collection, test): + """Test $polygon uses planar (flat) geometry.""" + 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..3b380b66 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_data_types.py @@ -0,0 +1,194 @@ +""" +Tests for $polygon data type coverage. + +Validates numeric types in coordinates, document location field types, +and embedded object and nested field path 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 assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +NUMERIC_COORDINATE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="int_coordinates", + 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 int coordinates should succeed", + ), + 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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(NUMERIC_COORDINATE_TESTS)) +def test_polygon_numeric_coordinate_types(collection, test): + """Test $polygon with various valid numeric coordinate types.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected) + + +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", + ), + 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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(LOCATION_FIELD_TYPE_TESTS)) +def test_polygon_location_field_types(collection, test): + """Test $polygon matching behavior with various location field types.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected) + + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(EMBEDDED_LOCATION_TESTS)) +def test_polygon_embedded_locations(collection, test): + """Test $polygon with embedded object and nested field path locations.""" + 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..a93e76b2 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_edge_cases.py @@ -0,0 +1,149 @@ +""" +Tests for $polygon edge cases. + +Validates degenerate polygons, boundary coordinates, special numeric values, +self-intersecting polygons, and duplicate points. +""" + +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 + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(DEGENERATE_POLYGON_TESTS)) +def test_polygon_degenerate_cases(collection, test): + """Test $polygon with degenerate polygon shapes.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True) + + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(BOUNDARY_COORDINATE_TESTS)) +def test_polygon_boundary_coordinates(collection, test): + """Test $polygon with boundary coordinate values.""" + 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_large_point_count(collection): + """Test $polygon with many points approximating a circle.""" + import math + + # Generate 100-point polygon approximating a circle of radius 50 centered at (50, 50) + points = [] + for i in range(100): + angle = 2 * math.pi * i / 100 + x = 50 + 50 * math.cos(angle) + y = 50 + 50 * math.sin(angle) + points.append([x, y]) + + collection.insert_many( + [ + {"_id": 1, "loc": [50, 50]}, # center - inside + {"_id": 2, "loc": [50, 75]}, # inside + {"_id": 3, "loc": [50, 110]}, # outside + ] + ) + result = execute_command( + collection, + {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$polygon": points}}}}, + ) + expected = [{"_id": 1, "loc": [50, 50]}, {"_id": 2, "loc": [50, 75]}] + assertSuccess(result, expected, ignore_doc_order=True, msg="Many-point polygon should work") 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..484845b8 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_errors.py @@ -0,0 +1,314 @@ +""" +Tests for $polygon error handling. + +Validates error codes for invalid operator usage, invalid polygon specifications, +minimum point requirements, 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, assertSuccess +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INVALID_OPERATOR_USAGE_TESTS)) +def test_polygon_invalid_operator_usage(collection, test): + """Test $polygon rejects invalid operator context.""" + collection.insert_many([{"_id": 1, "loc": [1, 1]}]) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertFailureCode(result, test.error_code) + + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INVALID_POLYGON_SPEC_TESTS)) +def test_polygon_invalid_specifications(collection, test): + """Test $polygon rejects invalid polygon specifications.""" + collection.insert_many([{"_id": 1, "loc": [1, 1]}]) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertFailureCode(result, test.error_code) + + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INVALID_ARGUMENT_FORMAT_TESTS)) +def test_polygon_invalid_argument_formats(collection, test): + """Test $polygon rejects non-array arguments.""" + collection.insert_many([{"_id": 1, "loc": [1, 1]}]) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertFailureCode(result, test.error_code) + + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INVALID_POINT_FORMAT_TESTS)) +def test_polygon_invalid_point_formats(collection, test): + """Test $polygon rejects invalid point formats.""" + collection.insert_many([{"_id": 1, "loc": [1, 1]}]) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertFailureCode(result, test.error_code) + + +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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INVALID_COORDINATE_TYPE_TESTS)) +def test_polygon_invalid_coordinate_types(collection, test): + """Test $polygon rejects non-numeric types in coordinate positions.""" + collection.insert_many([{"_id": 1, "loc": [1, 1]}]) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertFailureCode(result, test.error_code) + + +def test_polygon_in_expr_find_not_supported(collection): + """Test $polygon is not usable as geospatial filter inside $expr.""" + collection.insert_many([{"_id": 1, "loc": [5, 5]}]) + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "$expr": { + "$eq": [ + { + "$literal": { + "loc": { + "$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]} + } + } + }, + True, + ] + } + }, + }, + ) + assertSuccess( + result, [], msg="$polygon inside $expr should not match (not evaluated as geo query)" + ) + + +def test_polygon_in_expr_aggregate_not_supported(collection): + """Test $polygon is not usable as geospatial filter inside $expr in aggregation.""" + collection.insert_many([{"_id": 1, "loc": [5, 5]}]) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$match": { + "$expr": { + "$eq": [ + { + "$literal": { + "loc": { + "$geoWithin": { + "$polygon": [ + [0, 0], + [0, 10], + [10, 10], + [10, 0], + ] + } + } + } + }, + True, + ] + } + } + } + ], + "cursor": {}, + }, + ) + assertSuccess(result, [], msg="$polygon inside $expr in aggregate should not match") 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..4f99b336 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_index_interaction.py @@ -0,0 +1,132 @@ +""" +Tests for $polygon index interaction. + +Validates behavior with and without geospatial indexes. +""" + +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command + + +def test_polygon_without_index(collection): + """Test $polygon query succeeds without any geospatial index.""" + 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 without index") + + +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_results_match_with_and_without_index(collection): + """Test $polygon produces same results with and without 2d index.""" + docs = [ + {"_id": 1, "loc": [2, 3]}, + {"_id": 2, "loc": [7, 8]}, + {"_id": 3, "loc": [5, 5]}, + {"_id": 4, "loc": [15, 15]}, + ] + polygon_filter = {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}} + + # Query without index + collection.insert_many(docs) + execute_command(collection, {"find": collection.name, "filter": polygon_filter}) + + # Add index and query again + collection.create_index([("loc", "2d")]) + result_with_index = execute_command( + collection, {"find": collection.name, "filter": polygon_filter} + ) + + expected = [ + {"_id": 1, "loc": [2, 3]}, + {"_id": 2, "loc": [7, 8]}, + {"_id": 3, "loc": [5, 5]}, + ] + assertSuccess( + result_with_index, + expected, + ignore_doc_order=True, + msg="Results with index should match expected", + ) + + +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", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_js_specs.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_js_specs.py new file mode 100644 index 00000000..2676f8a1 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_js_specs.py @@ -0,0 +1,144 @@ +""" +Tests for $polygon from MongoDB JS test specifications. + +Covers geo_polygon1.js, geo_poly_edge.js, and geo_max.js test scenarios +including triangle containment, large coordinates, boundary coordinates, +and dense grid behavior. +""" + +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command + + +def test_polygon_triangle_single_match(collection): + """Test $polygon with triangle containing single point from grid (geo_polygon1).""" + collection.create_index([("loc", "2d")]) + # Insert a 10x10 grid + docs = [] + doc_id = 1 + for x in range(10): + for y in range(10): + docs.append({"_id": doc_id, "loc": [x, y]}) + doc_id += 1 + collection.insert_many(docs) + + # Small triangle containing only point [1,1] + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$polygon": [[0.5, 0.5], [1.5, 0.5], [1, 1.5]]}}}, + }, + ) + expected = [{"_id": 12, "loc": [1, 1]}] + assertSuccess(result, expected, msg="Triangle should contain single grid point") + + +def test_polygon_edge_large_coordinates(collection): + """Test $polygon with coordinates exceeding default 2d bounds (geo_poly_edge).""" + collection.create_index([("loc", "2d"), ("val", 1)], min=-1000, max=1000) + collection.insert_many( + [ + {"_id": 1, "loc": [100, 100], "val": 1}, + {"_id": 2, "loc": [500, 500], "val": 2}, + {"_id": 3, "loc": [900, 900], "val": 3}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 600], [600, 600], [600, 0]]}} + }, + }, + ) + expected = [ + {"_id": 1, "loc": [100, 100], "val": 1}, + {"_id": 2, "loc": [500, 500], "val": 2}, + ] + assertSuccess( + result, + expected, + ignore_doc_order=True, + msg="Large coordinates within custom bounds should work", + ) + + +def test_polygon_boundary_positive_max(collection): + """Test $polygon at positive max longitude boundary (geo_max).""" + collection.create_index([("loc", "2d")]) + collection.insert_many( + [ + {"_id": 1, "loc": [50, 50]}, + {"_id": 2, "loc": [-50, -50]}, + ] + ) + # Triangle at positive boundary + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 179], [179, 179], [179, 0]]}} + }, + }, + ) + expected = [{"_id": 1, "loc": [50, 50]}] + assertSuccess( + result, + expected, + msg="Polygon at positive boundary should match positive-x points only", + ) + + +def test_polygon_boundary_negative_max(collection): + """Test $polygon at negative max longitude boundary (geo_max).""" + collection.create_index([("loc", "2d")]) + collection.insert_many( + [ + {"_id": 1, "loc": [50, 50]}, + {"_id": 2, "loc": [-50, -50]}, + ] + ) + # Triangle at negative boundary + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "loc": {"$geoWithin": {"$polygon": [[-179, -179], [-179, 0], [0, 0], [0, -179]]}} + }, + }, + ) + expected = [{"_id": 2, "loc": [-50, -50]}] + assertSuccess( + result, + expected, + msg="Polygon at negative boundary should match negative-x points only", + ) + + +def test_polygon_dense_grid_triangle(collection): + """Test $polygon with triangle on dense grid (geo_polygon.js).""" + 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..111a96d5 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_query_interaction.py @@ -0,0 +1,452 @@ +""" +Tests for $polygon query context interaction. + +Validates $polygon in find, aggregation $match, combined with other operators, +and in update/delete filters. +""" + +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command + + +def test_polygon_in_find(collection): + """Test $polygon in basic find query.""" + 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 in find should return matching docs") + + +def test_polygon_in_aggregate_match(collection): + """Test $polygon in aggregation $match stage.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [15, 15]}, + ] + ) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + { + "$match": { + "loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}} + } + }, + ], + "cursor": {}, + }, + ) + expected = [{"_id": 1, "loc": [5, 5]}] + assertSuccess(result, expected, msg="$polygon in $match should return matching docs") + + +def test_polygon_combined_with_and(collection): + """Test $polygon combined with $and operator.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5], "status": "active"}, + {"_id": 2, "loc": [5, 5], "status": "inactive"}, + {"_id": 3, "loc": [15, 15], "status": "active"}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "$and": [ + {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + {"status": "active"}, + ] + }, + }, + ) + expected = [{"_id": 1, "loc": [5, 5], "status": "active"}] + assertSuccess(result, expected, msg="$polygon with $and should filter correctly") + + +def test_polygon_combined_with_or(collection): + """Test $polygon combined with $or operator.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [15, 15]}, + {"_id": 3, "loc": [25, 25]}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "$or": [ + {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + {"loc": {"$geoWithin": {"$polygon": [[20, 20], [20, 30], [30, 30], [30, 20]]}}}, + ] + }, + }, + ) + expected = [{"_id": 1, "loc": [5, 5]}, {"_id": 3, "loc": [25, 25]}] + assertSuccess( + result, expected, ignore_doc_order=True, msg="$polygon with $or should match either polygon" + ) + + +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_in_update_filter(collection): + """Test $polygon as filter in updateMany.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5], "updated": False}, + {"_id": 2, "loc": [15, 15], "updated": False}, + ] + ) + result = execute_command( + collection, + { + "update": collection.name, + "updates": [ + { + "q": { + "loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}} + }, + "u": {"$set": {"updated": True}}, + "multi": True, + } + ], + }, + ) + assertSuccess( + result, + {"n": 1, "nModified": 1, "ok": 1.0}, + raw_res=True, + msg="$polygon in update should match correct docs", + ) + + +def test_polygon_in_delete_filter(collection): + """Test $polygon as filter in delete.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [15, 15]}, + ] + ) + result = execute_command( + collection, + { + "delete": collection.name, + "deletes": [ + { + "q": { + "loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}} + }, + "limit": 0, + } + ], + }, + ) + assertSuccess( + result, + {"n": 1, "ok": 1.0}, + raw_res=True, + msg="$polygon in delete should match correct docs", + ) + + +def test_polygon_count_documents(collection): + """Test countDocuments with $polygon filter.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [3, 3]}, + {"_id": 3, "loc": [15, 15]}, + ] + ) + result = execute_command( + collection, + { + "count": collection.name, + "query": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + }, + ) + assertSuccess( + result, + {"n": 2, "ok": 1.0}, + raw_res=True, + msg="count with $polygon should return correct count", + ) + + +def test_polygon_distinct(collection): + """Test distinct with $polygon filter.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5], "category": "A"}, + {"_id": 2, "loc": [3, 3], "category": "B"}, + {"_id": 3, "loc": [15, 15], "category": "C"}, + ] + ) + result = execute_command( + collection, + { + "distinct": collection.name, + "key": "category", + "query": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + }, + ) + assertSuccess( + result, + {"values": ["A", "B"], "ok": 1.0}, + raw_res=True, + msg="distinct with $polygon should return correct values", + ) + + +def test_polygon_nested_field_path(collection): + """Test $polygon on nested field path.""" + collection.insert_many( + [ + {"_id": 1, "address": {"loc": [5, 5]}}, + {"_id": 2, "address": {"loc": [15, 15]}}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "address.loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}} + }, + }, + ) + expected = [{"_id": 1, "address": {"loc": [5, 5]}}] + assertSuccess(result, expected, msg="$polygon on nested field should work") + + +def test_polygon_empty_collection(collection): + """Test $polygon on empty collection returns empty result.""" + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + }, + ) + assertSuccess(result, [], msg="$polygon on empty collection should return empty result") + + +def test_polygon_combined_with_box_via_or(collection): + """Test $polygon combined with $box in same query via $or.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [25, 25]}, + {"_id": 3, "loc": [50, 50]}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "$or": [ + {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + {"loc": {"$geoWithin": {"$box": [[20, 20], [30, 30]]}}}, + ] + }, + }, + ) + expected = [{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [25, 25]}] + assertSuccess( + result, expected, ignore_doc_order=True, msg="$polygon and $box via $or should both work" + ) + + +def test_polygon_combined_with_center_via_or(collection): + """Test $polygon combined with $center in same query via $or.""" + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [25, 25]}, + {"_id": 3, "loc": [50, 50]}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "$or": [ + {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + {"loc": {"$geoWithin": {"$center": [[25, 25], 2]}}}, + ] + }, + }, + ) + expected = [{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [25, 25]}] + assertSuccess( + result, expected, ignore_doc_order=True, msg="$polygon and $center via $or should both work" + ) + + +def test_polygon_vs_geometry_polygon_different(collection): + """Test $polygon (legacy) and $geometry Polygon match different document types.""" + collection.create_index([("loc", "2dsphere")]) + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, # legacy coordinate pair + {"_id": 2, "loc": {"type": "Point", "coordinates": [5, 5]}}, # GeoJSON + ] + ) + # $polygon (legacy) - matches both legacy pairs and GeoJSON points + result_polygon = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + }, + ) + expected = [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [5, 5]}}, + ] + assertSuccess( + result_polygon, + expected, + ignore_doc_order=True, + msg="$polygon should match both legacy and GeoJSON point formats", + ) + + +def test_polygon_on_array_location_field(collection): + """Test $polygon on field that is an array of coordinate pairs.""" + collection.insert_many( + [ + {"_id": 1, "loc": [[5, 5], [15, 15]]}, + {"_id": 2, "loc": [[20, 20]]}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + }, + ) + # Array of coordinate pairs matches if any sub-pair is inside the polygon + expected = [{"_id": 1, "loc": [[5, 5], [15, 15]]}] + assertSuccess(result, expected, msg="Array location field should match if any point is inside") + + +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) From c2beba902225601ce862b3d6e00611f78594eb08 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 15 May 2026 09:42:31 -0700 Subject: [PATCH 2/5] Changed tests to use QueryTestCase Signed-off-by: PatersonProjects --- .../polygon/test_polygon_query_interaction.py | 299 ++++++++---------- 1 file changed, 124 insertions(+), 175 deletions(-) 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 index 111a96d5..da604b9c 100644 --- 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 @@ -5,27 +5,135 @@ and in update/delete filters. """ +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 - - -def test_polygon_in_find(collection): - """Test $polygon in basic find query.""" - collection.insert_many( - [ +from documentdb_tests.framework.parametrize import pytest_params + +FIND_QUERY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="basic_find", + 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="$polygon in find should return matching docs", + ), + 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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_QUERY_TESTS)) +def test_polygon_find_queries(collection, test): + """Test $polygon in various find query contexts.""" + if test.doc: + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected) + + +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]}, - ] - ) - result = execute_command( - collection, - { - "find": collection.name, - "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + {"_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]]}}}, + ] }, - ) - expected = [{"_id": 1, "loc": [5, 5]}] - assertSuccess(result, expected, msg="$polygon in find should return matching docs") + 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", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(COMBINED_OPERATOR_TESTS)) +def test_polygon_combined_operators(collection, test): + """Test $polygon combined with other query operators.""" + 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_in_aggregate_match(collection): @@ -54,58 +162,6 @@ def test_polygon_in_aggregate_match(collection): assertSuccess(result, expected, msg="$polygon in $match should return matching docs") -def test_polygon_combined_with_and(collection): - """Test $polygon combined with $and operator.""" - collection.insert_many( - [ - {"_id": 1, "loc": [5, 5], "status": "active"}, - {"_id": 2, "loc": [5, 5], "status": "inactive"}, - {"_id": 3, "loc": [15, 15], "status": "active"}, - ] - ) - result = execute_command( - collection, - { - "find": collection.name, - "filter": { - "$and": [ - {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, - {"status": "active"}, - ] - }, - }, - ) - expected = [{"_id": 1, "loc": [5, 5], "status": "active"}] - assertSuccess(result, expected, msg="$polygon with $and should filter correctly") - - -def test_polygon_combined_with_or(collection): - """Test $polygon combined with $or operator.""" - collection.insert_many( - [ - {"_id": 1, "loc": [5, 5]}, - {"_id": 2, "loc": [15, 15]}, - {"_id": 3, "loc": [25, 25]}, - ] - ) - result = execute_command( - collection, - { - "find": collection.name, - "filter": { - "$or": [ - {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, - {"loc": {"$geoWithin": {"$polygon": [[20, 20], [20, 30], [30, 30], [30, 20]]}}}, - ] - }, - }, - ) - expected = [{"_id": 1, "loc": [5, 5]}, {"_id": 3, "loc": [25, 25]}] - assertSuccess( - result, expected, ignore_doc_order=True, msg="$polygon with $or should match either polygon" - ) - - def test_polygon_with_projection(collection): """Test $polygon with field projection.""" collection.insert_many( @@ -289,93 +345,6 @@ def test_polygon_distinct(collection): ) -def test_polygon_nested_field_path(collection): - """Test $polygon on nested field path.""" - collection.insert_many( - [ - {"_id": 1, "address": {"loc": [5, 5]}}, - {"_id": 2, "address": {"loc": [15, 15]}}, - ] - ) - result = execute_command( - collection, - { - "find": collection.name, - "filter": { - "address.loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}} - }, - }, - ) - expected = [{"_id": 1, "address": {"loc": [5, 5]}}] - assertSuccess(result, expected, msg="$polygon on nested field should work") - - -def test_polygon_empty_collection(collection): - """Test $polygon on empty collection returns empty result.""" - result = execute_command( - collection, - { - "find": collection.name, - "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, - }, - ) - assertSuccess(result, [], msg="$polygon on empty collection should return empty result") - - -def test_polygon_combined_with_box_via_or(collection): - """Test $polygon combined with $box in same query via $or.""" - collection.insert_many( - [ - {"_id": 1, "loc": [5, 5]}, - {"_id": 2, "loc": [25, 25]}, - {"_id": 3, "loc": [50, 50]}, - ] - ) - result = execute_command( - collection, - { - "find": collection.name, - "filter": { - "$or": [ - {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, - {"loc": {"$geoWithin": {"$box": [[20, 20], [30, 30]]}}}, - ] - }, - }, - ) - expected = [{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [25, 25]}] - assertSuccess( - result, expected, ignore_doc_order=True, msg="$polygon and $box via $or should both work" - ) - - -def test_polygon_combined_with_center_via_or(collection): - """Test $polygon combined with $center in same query via $or.""" - collection.insert_many( - [ - {"_id": 1, "loc": [5, 5]}, - {"_id": 2, "loc": [25, 25]}, - {"_id": 3, "loc": [50, 50]}, - ] - ) - result = execute_command( - collection, - { - "find": collection.name, - "filter": { - "$or": [ - {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, - {"loc": {"$geoWithin": {"$center": [[25, 25], 2]}}}, - ] - }, - }, - ) - expected = [{"_id": 1, "loc": [5, 5]}, {"_id": 2, "loc": [25, 25]}] - assertSuccess( - result, expected, ignore_doc_order=True, msg="$polygon and $center via $or should both work" - ) - - def test_polygon_vs_geometry_polygon_different(collection): """Test $polygon (legacy) and $geometry Polygon match different document types.""" collection.create_index([("loc", "2dsphere")]) @@ -405,26 +374,6 @@ def test_polygon_vs_geometry_polygon_different(collection): ) -def test_polygon_on_array_location_field(collection): - """Test $polygon on field that is an array of coordinate pairs.""" - collection.insert_many( - [ - {"_id": 1, "loc": [[5, 5], [15, 15]]}, - {"_id": 2, "loc": [[20, 20]]}, - ] - ) - result = execute_command( - collection, - { - "find": collection.name, - "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, - }, - ) - # Array of coordinate pairs matches if any sub-pair is inside the polygon - expected = [{"_id": 1, "loc": [[5, 5], [15, 15]]}] - assertSuccess(result, expected, msg="Array location field should match if any point is inside") - - def test_polygon_on_capped_collection(database_client): """Test $polygon on capped collection.""" coll_name = "test_polygon_capped" From 09f2756a818bd92a7791d33113ceb75b1b777ecf Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 15 May 2026 13:09:23 -0700 Subject: [PATCH 3/5] First pass of fixes, cleaned up tests to fit QueryTestCase, removed overlaps Signed-off-by: PatersonProjects --- .../query/geospatial/specifiers/__init__.py | 0 .../geospatial/specifiers/polygon/__init__.py | 0 .../test_polygon_core_functionality.py | 90 ++------ .../polygon/test_polygon_data_types.py | 34 +-- .../polygon/test_polygon_edge_cases.py | 100 +++++---- .../specifiers/polygon/test_polygon_errors.py | 131 +++--------- .../polygon/test_polygon_index_interaction.py | 80 +++---- .../polygon/test_polygon_js_specs.py | 144 ------------- .../polygon/test_polygon_query_interaction.py | 197 ++---------------- 9 files changed, 169 insertions(+), 607 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/__init__.py delete mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_js_specs.py 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 index fc9004eb..1aeb7072 100644 --- 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 @@ -2,7 +2,8 @@ Tests for $polygon core geometric behavior. Validates valid point counts, point containment, concave polygon shapes, -winding order invariance, implicit closure, and coordinate conventions. +winding order invariance, implicit and explicit closure, coordinate +behavior, and planar geometry. """ import pytest @@ -38,15 +39,6 @@ ), ] - -@pytest.mark.parametrize("test", pytest_params(VALID_POINT_COUNT_TESTS)) -def test_polygon_valid_point_counts(collection, test): - """Test $polygon succeeds with 3 or more points.""" - collection.insert_many(test.doc) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertSuccess(result, test.expected) - - POINT_CONTAINMENT_TESTS: list[QueryTestCase] = [ QueryTestCase( id="point_inside_triangle", @@ -94,42 +86,15 @@ def test_polygon_valid_point_counts(collection, test): ), ] - -@pytest.mark.parametrize("test", pytest_params(POINT_CONTAINMENT_TESTS)) -def test_polygon_point_containment(collection, test): - """Test $polygon correctly identifies points inside/outside polygon.""" - 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_winding_order_invariance(collection): - """Test $polygon produces same results regardless of winding order.""" - collection.insert_many( - [ - {"_id": 1, "loc": [5, 5]}, - {"_id": 2, "loc": [15, 15]}, - ] - ) - # Clockwise - result_cw = execute_command( - collection, - { - "find": collection.name, - "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [10, 0], [10, 10], [0, 10]]}}}, - }, - ) - # Counter-clockwise (verify it also succeeds) - 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_cw, expected, msg="Clockwise winding should match point inside") - +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( @@ -148,15 +113,6 @@ def test_polygon_winding_order_invariance(collection): ), ] - -@pytest.mark.parametrize("test", pytest_params(CLOSURE_TESTS)) -def test_polygon_closure(collection, test): - """Test $polygon implicit and explicit polygon closure.""" - collection.insert_many(test.doc) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertSuccess(result, test.expected) - - COORDINATE_BEHAVIOR_TESTS: list[QueryTestCase] = [ QueryTestCase( id="negative_coordinates", @@ -188,15 +144,6 @@ def test_polygon_closure(collection, test): ), ] - -@pytest.mark.parametrize("test", pytest_params(COORDINATE_BEHAVIOR_TESTS)) -def test_polygon_coordinate_behavior(collection, test): - """Test $polygon with various coordinate ranges and conventions.""" - collection.insert_many(test.doc) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertSuccess(result, test.expected) - - PLANAR_GEOMETRY_TESTS: list[QueryTestCase] = [ QueryTestCase( id="antimeridian_no_wrap", @@ -232,10 +179,19 @@ def test_polygon_coordinate_behavior(collection, test): ), ] +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(PLANAR_GEOMETRY_TESTS)) -def test_polygon_planar_geometry(collection, test): - """Test $polygon uses planar (flat) geometry.""" +@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 index 3b380b66..c2bbe98f 100644 --- 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 @@ -16,13 +16,6 @@ from documentdb_tests.framework.parametrize import pytest_params NUMERIC_COORDINATE_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="int_coordinates", - 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 int coordinates should succeed", - ), QueryTestCase( id="double_coordinates", filter={"loc": {"$geoWithin": {"$polygon": [[0.0, 0.0], [3.5, 6.5], [7.0, 0.0]]}}}, @@ -84,14 +77,6 @@ ] -@pytest.mark.parametrize("test", pytest_params(NUMERIC_COORDINATE_TESTS)) -def test_polygon_numeric_coordinate_types(collection, test): - """Test $polygon with various valid numeric coordinate types.""" - collection.insert_many(test.doc) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertSuccess(result, test.expected) - - LOCATION_FIELD_TYPE_TESTS: list[QueryTestCase] = [ QueryTestCase( id="legacy_coordinate_pair", @@ -152,14 +137,6 @@ def test_polygon_numeric_coordinate_types(collection, test): ] -@pytest.mark.parametrize("test", pytest_params(LOCATION_FIELD_TYPE_TESTS)) -def test_polygon_location_field_types(collection, test): - """Test $polygon matching behavior with various location field types.""" - collection.insert_many(test.doc) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertSuccess(result, test.expected) - - EMBEDDED_LOCATION_TESTS: list[QueryTestCase] = [ QueryTestCase( id="embedded_object_location", @@ -186,9 +163,14 @@ def test_polygon_location_field_types(collection, test): ] -@pytest.mark.parametrize("test", pytest_params(EMBEDDED_LOCATION_TESTS)) -def test_polygon_embedded_locations(collection, test): - """Test $polygon with embedded object and nested field path locations.""" +DATA_TYPE_TESTS: list[QueryTestCase] = ( + NUMERIC_COORDINATE_TESTS + LOCATION_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 index a93e76b2..ffa0cfd7 100644 --- 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 @@ -1,10 +1,13 @@ """ Tests for $polygon edge cases. -Validates degenerate polygons, boundary coordinates, special numeric values, -self-intersecting polygons, and duplicate points. +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 ( @@ -14,6 +17,12 @@ 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", @@ -64,14 +73,6 @@ ] -@pytest.mark.parametrize("test", pytest_params(DEGENERATE_POLYGON_TESTS)) -def test_polygon_degenerate_cases(collection, test): - """Test $polygon with degenerate polygon shapes.""" - collection.insert_many(test.doc) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertSuccess(result, test.expected, ignore_doc_order=True) - - BOUNDARY_COORDINATE_TESTS: list[QueryTestCase] = [ QueryTestCase( id="coordinates_at_zero", @@ -114,36 +115,57 @@ def test_polygon_degenerate_cases(collection, test): ] -@pytest.mark.parametrize("test", pytest_params(BOUNDARY_COORDINATE_TESTS)) -def test_polygon_boundary_coordinates(collection, test): - """Test $polygon with boundary coordinate values.""" - 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_large_point_count(collection): - """Test $polygon with many points approximating a circle.""" - import math - - # Generate 100-point polygon approximating a circle of radius 50 centered at (50, 50) - points = [] - for i in range(100): - angle = 2 * math.pi * i / 100 - x = 50 + 50 * math.cos(angle) - y = 50 + 50 * math.sin(angle) - points.append([x, y]) +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", + ), +] - collection.insert_many( - [ +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 - ] - ) - result = execute_command( - collection, - {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$polygon": points}}}}, - ) - expected = [{"_id": 1, "loc": [50, 50]}, {"_id": 2, "loc": [50, 75]}] - assertSuccess(result, expected, ignore_doc_order=True, msg="Many-point polygon should work") + ], + 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 index 484845b8..5efe8ad3 100644 --- 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 @@ -2,8 +2,8 @@ Tests for $polygon error handling. Validates error codes for invalid operator usage, invalid polygon specifications, -minimum point requirements, invalid argument formats, invalid point formats, -invalid coordinate types, and special numeric values in coordinates. +invalid argument formats, invalid point formats, invalid coordinate types, +and special numeric values in coordinates. """ from datetime import datetime, timezone @@ -14,7 +14,7 @@ from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( QueryTestCase, ) -from documentdb_tests.framework.assertions import assertFailureCode, assertSuccess +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 @@ -36,14 +36,6 @@ ] -@pytest.mark.parametrize("test", pytest_params(INVALID_OPERATOR_USAGE_TESTS)) -def test_polygon_invalid_operator_usage(collection, test): - """Test $polygon rejects invalid operator context.""" - collection.insert_many([{"_id": 1, "loc": [1, 1]}]) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertFailureCode(result, test.error_code) - - INVALID_POLYGON_SPEC_TESTS: list[QueryTestCase] = [ QueryTestCase( id="empty_array", @@ -78,14 +70,6 @@ def test_polygon_invalid_operator_usage(collection, test): ] -@pytest.mark.parametrize("test", pytest_params(INVALID_POLYGON_SPEC_TESTS)) -def test_polygon_invalid_specifications(collection, test): - """Test $polygon rejects invalid polygon specifications.""" - collection.insert_many([{"_id": 1, "loc": [1, 1]}]) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertFailureCode(result, test.error_code) - - INVALID_ARGUMENT_FORMAT_TESTS: list[QueryTestCase] = [ QueryTestCase( id="int_arg", @@ -108,14 +92,6 @@ def test_polygon_invalid_specifications(collection, test): ] -@pytest.mark.parametrize("test", pytest_params(INVALID_ARGUMENT_FORMAT_TESTS)) -def test_polygon_invalid_argument_formats(collection, test): - """Test $polygon rejects non-array arguments.""" - collection.insert_many([{"_id": 1, "loc": [1, 1]}]) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertFailureCode(result, test.error_code) - - INVALID_POINT_FORMAT_TESTS: list[QueryTestCase] = [ QueryTestCase( id="flat_array_not_nested", @@ -162,14 +138,6 @@ def test_polygon_invalid_argument_formats(collection, test): ] -@pytest.mark.parametrize("test", pytest_params(INVALID_POINT_FORMAT_TESTS)) -def test_polygon_invalid_point_formats(collection, test): - """Test $polygon rejects invalid point formats.""" - collection.insert_many([{"_id": 1, "loc": [1, 1]}]) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertFailureCode(result, test.error_code) - - INVALID_COORDINATE_TYPE_TESTS: list[QueryTestCase] = [ QueryTestCase( id="objectid_in_coordinate", @@ -237,78 +205,33 @@ def test_polygon_invalid_point_formats(collection, test): 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", + ), ] -@pytest.mark.parametrize("test", pytest_params(INVALID_COORDINATE_TYPE_TESTS)) -def test_polygon_invalid_coordinate_types(collection, test): - """Test $polygon rejects non-numeric types in coordinate positions.""" +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) - - -def test_polygon_in_expr_find_not_supported(collection): - """Test $polygon is not usable as geospatial filter inside $expr.""" - collection.insert_many([{"_id": 1, "loc": [5, 5]}]) - result = execute_command( - collection, - { - "find": collection.name, - "filter": { - "$expr": { - "$eq": [ - { - "$literal": { - "loc": { - "$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]} - } - } - }, - True, - ] - } - }, - }, - ) - assertSuccess( - result, [], msg="$polygon inside $expr should not match (not evaluated as geo query)" - ) - - -def test_polygon_in_expr_aggregate_not_supported(collection): - """Test $polygon is not usable as geospatial filter inside $expr in aggregation.""" - collection.insert_many([{"_id": 1, "loc": [5, 5]}]) - result = execute_command( - collection, - { - "aggregate": collection.name, - "pipeline": [ - { - "$match": { - "$expr": { - "$eq": [ - { - "$literal": { - "loc": { - "$geoWithin": { - "$polygon": [ - [0, 0], - [0, 10], - [10, 10], - [10, 0], - ] - } - } - } - }, - True, - ] - } - } - } - ], - "cursor": {}, - }, - ) - assertSuccess(result, [], msg="$polygon inside $expr in aggregate should not match") 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 index 4f99b336..c688db41 100644 --- 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 @@ -1,32 +1,14 @@ """ Tests for $polygon index interaction. -Validates behavior with and without geospatial indexes. +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_without_index(collection): - """Test $polygon query succeeds without any geospatial index.""" - 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 without index") - - def test_polygon_with_2d_index(collection): """Test $polygon query succeeds with 2d index.""" collection.create_index([("loc", "2d")]) @@ -47,39 +29,6 @@ def test_polygon_with_2d_index(collection): assertSuccess(result, expected, msg="$polygon should work with 2d index") -def test_polygon_results_match_with_and_without_index(collection): - """Test $polygon produces same results with and without 2d index.""" - docs = [ - {"_id": 1, "loc": [2, 3]}, - {"_id": 2, "loc": [7, 8]}, - {"_id": 3, "loc": [5, 5]}, - {"_id": 4, "loc": [15, 15]}, - ] - polygon_filter = {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}} - - # Query without index - collection.insert_many(docs) - execute_command(collection, {"find": collection.name, "filter": polygon_filter}) - - # Add index and query again - collection.create_index([("loc", "2d")]) - result_with_index = execute_command( - collection, {"find": collection.name, "filter": polygon_filter} - ) - - expected = [ - {"_id": 1, "loc": [2, 3]}, - {"_id": 2, "loc": [7, 8]}, - {"_id": 3, "loc": [5, 5]}, - ] - assertSuccess( - result_with_index, - expected, - ignore_doc_order=True, - msg="Results with index should match expected", - ) - - 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")]) @@ -130,3 +79,28 @@ def test_polygon_with_2d_index_precision(collection): 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_js_specs.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_js_specs.py deleted file mode 100644 index 2676f8a1..00000000 --- a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/polygon/test_polygon_js_specs.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Tests for $polygon from MongoDB JS test specifications. - -Covers geo_polygon1.js, geo_poly_edge.js, and geo_max.js test scenarios -including triangle containment, large coordinates, boundary coordinates, -and dense grid behavior. -""" - -from documentdb_tests.framework.assertions import assertSuccess -from documentdb_tests.framework.executor import execute_command - - -def test_polygon_triangle_single_match(collection): - """Test $polygon with triangle containing single point from grid (geo_polygon1).""" - collection.create_index([("loc", "2d")]) - # Insert a 10x10 grid - docs = [] - doc_id = 1 - for x in range(10): - for y in range(10): - docs.append({"_id": doc_id, "loc": [x, y]}) - doc_id += 1 - collection.insert_many(docs) - - # Small triangle containing only point [1,1] - result = execute_command( - collection, - { - "find": collection.name, - "filter": {"loc": {"$geoWithin": {"$polygon": [[0.5, 0.5], [1.5, 0.5], [1, 1.5]]}}}, - }, - ) - expected = [{"_id": 12, "loc": [1, 1]}] - assertSuccess(result, expected, msg="Triangle should contain single grid point") - - -def test_polygon_edge_large_coordinates(collection): - """Test $polygon with coordinates exceeding default 2d bounds (geo_poly_edge).""" - collection.create_index([("loc", "2d"), ("val", 1)], min=-1000, max=1000) - collection.insert_many( - [ - {"_id": 1, "loc": [100, 100], "val": 1}, - {"_id": 2, "loc": [500, 500], "val": 2}, - {"_id": 3, "loc": [900, 900], "val": 3}, - ] - ) - result = execute_command( - collection, - { - "find": collection.name, - "filter": { - "loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 600], [600, 600], [600, 0]]}} - }, - }, - ) - expected = [ - {"_id": 1, "loc": [100, 100], "val": 1}, - {"_id": 2, "loc": [500, 500], "val": 2}, - ] - assertSuccess( - result, - expected, - ignore_doc_order=True, - msg="Large coordinates within custom bounds should work", - ) - - -def test_polygon_boundary_positive_max(collection): - """Test $polygon at positive max longitude boundary (geo_max).""" - collection.create_index([("loc", "2d")]) - collection.insert_many( - [ - {"_id": 1, "loc": [50, 50]}, - {"_id": 2, "loc": [-50, -50]}, - ] - ) - # Triangle at positive boundary - result = execute_command( - collection, - { - "find": collection.name, - "filter": { - "loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 179], [179, 179], [179, 0]]}} - }, - }, - ) - expected = [{"_id": 1, "loc": [50, 50]}] - assertSuccess( - result, - expected, - msg="Polygon at positive boundary should match positive-x points only", - ) - - -def test_polygon_boundary_negative_max(collection): - """Test $polygon at negative max longitude boundary (geo_max).""" - collection.create_index([("loc", "2d")]) - collection.insert_many( - [ - {"_id": 1, "loc": [50, 50]}, - {"_id": 2, "loc": [-50, -50]}, - ] - ) - # Triangle at negative boundary - result = execute_command( - collection, - { - "find": collection.name, - "filter": { - "loc": {"$geoWithin": {"$polygon": [[-179, -179], [-179, 0], [0, 0], [0, -179]]}} - }, - }, - ) - expected = [{"_id": 2, "loc": [-50, -50]}] - assertSuccess( - result, - expected, - msg="Polygon at negative boundary should match negative-x points only", - ) - - -def test_polygon_dense_grid_triangle(collection): - """Test $polygon with triangle on dense grid (geo_polygon.js).""" - 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 index da604b9c..91353276 100644 --- 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 @@ -1,8 +1,9 @@ """ Tests for $polygon query context interaction. -Validates $polygon in find, aggregation $match, combined with other operators, -and in update/delete filters. +Validates $polygon in find with various options (projection, sort, limit), +combined with other operators, $expr non-support, and special collection +contexts. """ import pytest @@ -15,13 +16,6 @@ from documentdb_tests.framework.parametrize import pytest_params FIND_QUERY_TESTS: list[QueryTestCase] = [ - QueryTestCase( - id="basic_find", - 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="$polygon in find should return matching docs", - ), QueryTestCase( id="nested_field_path", filter={"address.loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, @@ -48,6 +42,26 @@ 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)", + ), ] @@ -136,32 +150,6 @@ def test_polygon_combined_operators(collection, test): assertSuccess(result, test.expected, ignore_doc_order=True) -def test_polygon_in_aggregate_match(collection): - """Test $polygon in aggregation $match stage.""" - collection.insert_many( - [ - {"_id": 1, "loc": [5, 5]}, - {"_id": 2, "loc": [15, 15]}, - ] - ) - result = execute_command( - collection, - { - "aggregate": collection.name, - "pipeline": [ - { - "$match": { - "loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}} - } - }, - ], - "cursor": {}, - }, - ) - expected = [{"_id": 1, "loc": [5, 5]}] - assertSuccess(result, expected, msg="$polygon in $match should return matching docs") - - def test_polygon_with_projection(collection): """Test $polygon with field projection.""" collection.insert_many( @@ -235,145 +223,6 @@ def test_polygon_with_limit(collection): ) -def test_polygon_in_update_filter(collection): - """Test $polygon as filter in updateMany.""" - collection.insert_many( - [ - {"_id": 1, "loc": [5, 5], "updated": False}, - {"_id": 2, "loc": [15, 15], "updated": False}, - ] - ) - result = execute_command( - collection, - { - "update": collection.name, - "updates": [ - { - "q": { - "loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}} - }, - "u": {"$set": {"updated": True}}, - "multi": True, - } - ], - }, - ) - assertSuccess( - result, - {"n": 1, "nModified": 1, "ok": 1.0}, - raw_res=True, - msg="$polygon in update should match correct docs", - ) - - -def test_polygon_in_delete_filter(collection): - """Test $polygon as filter in delete.""" - collection.insert_many( - [ - {"_id": 1, "loc": [5, 5]}, - {"_id": 2, "loc": [15, 15]}, - ] - ) - result = execute_command( - collection, - { - "delete": collection.name, - "deletes": [ - { - "q": { - "loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}} - }, - "limit": 0, - } - ], - }, - ) - assertSuccess( - result, - {"n": 1, "ok": 1.0}, - raw_res=True, - msg="$polygon in delete should match correct docs", - ) - - -def test_polygon_count_documents(collection): - """Test countDocuments with $polygon filter.""" - collection.insert_many( - [ - {"_id": 1, "loc": [5, 5]}, - {"_id": 2, "loc": [3, 3]}, - {"_id": 3, "loc": [15, 15]}, - ] - ) - result = execute_command( - collection, - { - "count": collection.name, - "query": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, - }, - ) - assertSuccess( - result, - {"n": 2, "ok": 1.0}, - raw_res=True, - msg="count with $polygon should return correct count", - ) - - -def test_polygon_distinct(collection): - """Test distinct with $polygon filter.""" - collection.insert_many( - [ - {"_id": 1, "loc": [5, 5], "category": "A"}, - {"_id": 2, "loc": [3, 3], "category": "B"}, - {"_id": 3, "loc": [15, 15], "category": "C"}, - ] - ) - result = execute_command( - collection, - { - "distinct": collection.name, - "key": "category", - "query": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, - }, - ) - assertSuccess( - result, - {"values": ["A", "B"], "ok": 1.0}, - raw_res=True, - msg="distinct with $polygon should return correct values", - ) - - -def test_polygon_vs_geometry_polygon_different(collection): - """Test $polygon (legacy) and $geometry Polygon match different document types.""" - collection.create_index([("loc", "2dsphere")]) - collection.insert_many( - [ - {"_id": 1, "loc": [5, 5]}, # legacy coordinate pair - {"_id": 2, "loc": {"type": "Point", "coordinates": [5, 5]}}, # GeoJSON - ] - ) - # $polygon (legacy) - matches both legacy pairs and GeoJSON points - result_polygon = execute_command( - collection, - { - "find": collection.name, - "filter": {"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, - }, - ) - expected = [ - {"_id": 1, "loc": [5, 5]}, - {"_id": 2, "loc": {"type": "Point", "coordinates": [5, 5]}}, - ] - assertSuccess( - result_polygon, - expected, - ignore_doc_order=True, - msg="$polygon should match both legacy and GeoJSON point formats", - ) - - def test_polygon_on_capped_collection(database_client): """Test $polygon on capped collection.""" coll_name = "test_polygon_capped" From 8d1b81b58f7b04eb0fa2f90161cda6d148769dfb Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 15 May 2026 13:45:26 -0700 Subject: [PATCH 4/5] Added missing coverage Signed-off-by: PatersonProjects --- .../polygon/test_polygon_data_types.py | 91 ++++++++++++++++++- .../polygon/test_polygon_query_interaction.py | 21 ++--- 2 files changed, 95 insertions(+), 17 deletions(-) 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 index c2bbe98f..ab075740 100644 --- 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 @@ -1,12 +1,15 @@ """ Tests for $polygon data type coverage. -Validates numeric types in coordinates, document location field types, -and embedded object and nested field path behavior. +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 Decimal128, Int64 +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, @@ -14,6 +17,7 @@ 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( @@ -99,6 +103,9 @@ ], 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]]}}}, @@ -134,6 +141,79 @@ 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", + ), ] @@ -164,7 +244,10 @@ DATA_TYPE_TESTS: list[QueryTestCase] = ( - NUMERIC_COORDINATE_TESTS + LOCATION_FIELD_TYPE_TESTS + EMBEDDED_LOCATION_TESTS + NUMERIC_COORDINATE_TESTS + + LOCATION_FIELD_TYPE_TESTS + + NON_MATCHING_FIELD_TYPE_TESTS + + EMBEDDED_LOCATION_TESTS ) 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 index 91353276..d7ee0848 100644 --- 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 @@ -65,15 +65,6 @@ ] -@pytest.mark.parametrize("test", pytest_params(FIND_QUERY_TESTS)) -def test_polygon_find_queries(collection, test): - """Test $polygon in various find query contexts.""" - if test.doc: - collection.insert_many(test.doc) - result = execute_command(collection, {"find": collection.name, "filter": test.filter}) - assertSuccess(result, test.expected) - - COMBINED_OPERATOR_TESTS: list[QueryTestCase] = [ QueryTestCase( id="with_and", @@ -142,10 +133,14 @@ def test_polygon_find_queries(collection, test): ] -@pytest.mark.parametrize("test", pytest_params(COMBINED_OPERATOR_TESTS)) -def test_polygon_combined_operators(collection, test): - """Test $polygon combined with other query operators.""" - collection.insert_many(test.doc) +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) From 5f3d04b8ea36b263182c317810d461c3b5ee5780 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 19 May 2026 15:36:52 -0700 Subject: [PATCH 5/5] Added PR Requested test cases Signed-off-by: PatersonProjects --- .../geoWithin/test_geoWithin_errors.py | 16 ++++++++- .../polygon/test_polygon_data_types.py | 14 ++++++++ .../polygon/test_polygon_edge_cases.py | 23 +++++++++++++ .../specifiers/polygon/test_polygon_errors.py | 34 ++++++++++++++++++- .../polygon/test_polygon_query_interaction.py | 20 +++++++++-- 5 files changed, 103 insertions(+), 4 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoWithin/test_geoWithin_errors.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoWithin/test_geoWithin_errors.py index 26d91a39..cd06bf49 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoWithin/test_geoWithin_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoWithin/test_geoWithin_errors.py @@ -1,5 +1,6 @@ """ -Tests for $geoWithin error cases — argument validation, coordinate validation, and invalid geometry. +Tests for $geoWithin error cases — argument validation, coordinate validation, +invalid geometry, CRS validation, and legacy shape specifier validation. """ import pytest @@ -353,6 +354,19 @@ error_code=BAD_VALUE_ERROR, msg="$polygon with fewer than 3 points should error", ), + QueryTestCase( + id="multiple_shape_specifiers", + filter={ + "loc": { + "$geoWithin": { + "$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]], + "$box": [[0, 0], [10, 10]], + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Multiple shape specifiers in one $geoWithin should error", + ), ] 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 index ab075740..6bc456d8 100644 --- 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 @@ -229,6 +229,20 @@ 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="embedded_doc_non_xy_keys", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": {"a": 5, "b": 5}}, + {"_id": 2, "loc": [5, 5]}, + {"_id": 3, "loc": [15, 15]}, + ], + expected=[ + {"_id": 1, "loc": {"a": 5, "b": 5}}, + {"_id": 2, "loc": [5, 5]}, + ], + msg="Embedded doc with non-x/y keys uses first two fields as x/y", + ), QueryTestCase( id="nested_field_missing_intermediate", filter={"address.loc": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}}, 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 index ffa0cfd7..068d456d 100644 --- 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 @@ -138,6 +138,29 @@ expected=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [5, 5]}], msg="Point on polygon vertex should match", ), + QueryTestCase( + id="point_on_diagonal_edge", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": [5, 5]}, # midpoint of hypotenuse [0,0]->[10,10] + {"_id": 2, "loc": [3, 3]}, # on hypotenuse + {"_id": 3, "loc": [5, 1]}, # inside triangle + {"_id": 4, "loc": [0, 10]}, # outside + ], + expected=[{"_id": 3, "loc": [5, 1]}], + msg="Point on non-axis-aligned (diagonal) edge is not included", + ), + QueryTestCase( + id="point_collinear_beyond_segment", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0], [10, 10], [10, 0]]}}}, + doc=[ + {"_id": 1, "loc": [15, 15]}, # collinear with [0,0]->[10,10] but beyond + {"_id": 2, "loc": [-5, -5]}, # collinear but before start + {"_id": 3, "loc": [5, 1]}, # inside triangle + ], + expected=[{"_id": 3, "loc": [5, 1]}], + msg="Point collinear with edge but beyond segment should not match", + ), ] LARGE_POINT_COUNT_TESTS: list[QueryTestCase] = [ 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 index 5efe8ad3..0b276bb9 100644 --- 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 @@ -18,7 +18,11 @@ 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 +from documentdb_tests.framework.test_constants import ( + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) INVALID_OPERATOR_USAGE_TESTS: list[QueryTestCase] = [ QueryTestCase( @@ -135,6 +139,18 @@ error_code=BAD_VALUE_ERROR, msg="$polygon with single-coordinate points should error", ), + QueryTestCase( + id="three_element_points", + filter={"loc": {"$geoWithin": {"$polygon": [[0, 0, 0], [1, 1, 1], [2, 0, 0]]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with 3-element coordinate points should error", + ), + QueryTestCase( + id="empty_sub_arrays_as_points", + filter={"loc": {"$geoWithin": {"$polygon": [[], [], []]}}}, + error_code=BAD_VALUE_ERROR, + msg="$polygon with empty sub-arrays as points should error", + ), ] @@ -205,6 +221,22 @@ error_code=BAD_VALUE_ERROR, msg="Infinity in coordinate should error", ), + QueryTestCase( + id="negative_infinity_in_coordinate", + filter={ + "loc": { + "$geoWithin": { + "$polygon": [ + [FLOAT_NEGATIVE_INFINITY, 0], + [1, 1], + [2, 0], + ] + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Negative infinity in coordinate should error", + ), QueryTestCase( id="object_in_coordinate", filter={"loc": {"$geoWithin": {"$polygon": [[{"a": 1}, 0], [1, 1], [2, 0]]}}}, 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 index d7ee0848..889747e8 100644 --- 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 @@ -2,8 +2,8 @@ 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. +combined with other operators ($not, $and, $or, $box, $center), +$expr non-support, and special collection contexts. """ import pytest @@ -66,6 +66,22 @@ COMBINED_OPERATOR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="with_not", + filter={ + "loc": {"$not": {"$geoWithin": {"$polygon": [[0, 0], [0, 10], [10, 10], [10, 0]]}}} + }, + doc=[ + {"_id": 1, "loc": [5, 5]}, + {"_id": 2, "loc": [15, 15]}, + {"_id": 3, "loc": [25, 25]}, + ], + expected=[ + {"_id": 2, "loc": [15, 15]}, + {"_id": 3, "loc": [25, 25]}, + ], + msg="$not with $polygon should return points outside the polygon", + ), QueryTestCase( id="with_and", filter={