From 25f7f979a9f297399fc5d8202ade19d8aa8e8496 Mon Sep 17 00:00:00 2001 From: Victor Tsang Date: Thu, 28 May 2026 14:07:40 -0700 Subject: [PATCH 1/2] Added geospatial tests for $geoIntersects Signed-off-by: Victor Tsang --- .../geospatial/geoIntersects/__init__.py | 0 ...test_geoIntersects_bson_type_validation.py | 102 ++ .../geoIntersects/test_geoIntersects_core.py | 913 ++++++++++++++++++ .../test_geoIntersects_edge_cases.py | 424 ++++++++ .../test_geoIntersects_errors.py | 681 +++++++++++++ .../test_geoIntersects_query_context.py | 238 +++++ documentdb_tests/framework/error_codes.py | 1 + 7 files changed, 2359 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_bson_type_validation.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_core.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_edge_cases.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_query_context.py diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/__init__.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_bson_type_validation.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_bson_type_validation.py new file mode 100644 index 000000000..18b5aab22 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_bson_type_validation.py @@ -0,0 +1,102 @@ +""" +Tests for $geoIntersects BSON type validation of the $geometry argument and its fields. + +Verifies that $geoIntersects rejects invalid BSON types for $geometry, its "type" +field, and its "coordinates" field with expected error codes. +""" + +import pytest + +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccess +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR +from documentdb_tests.framework.executor import execute_command + +VALID_POLYGON_COORDS = [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]] + +GEOINTERSECTS_PARAMS = [ + BsonTypeTestCase( + id="geometry", + msg="$geometry should reject non-object types", + keyword="$geometry", + valid_types=[BsonType.OBJECT], + default_error_code=BAD_VALUE_ERROR, + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + valid_inputs={BsonType.OBJECT: {"type": "Polygon", "coordinates": VALID_POLYGON_COORDS}}, + ), + BsonTypeTestCase( + id="geometry_type", + msg="$geometry 'type' field should reject non-string types", + keyword="type", + valid_types=[BsonType.STRING], + default_error_code=BAD_VALUE_ERROR, + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + valid_inputs={BsonType.STRING: "Polygon"}, + ), + BsonTypeTestCase( + id="geometry_coordinates", + msg="$geometry 'coordinates' field should reject non-array types", + keyword="coordinates", + valid_types=[BsonType.ARRAY], + default_error_code=BAD_VALUE_ERROR, + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + valid_inputs={BsonType.ARRAY: VALID_POLYGON_COORDS}, + ), + BsonTypeTestCase( + id="geometry_crs", + msg="$geometry 'crs' field should reject non-object types", + keyword="crs", + valid_types=[BsonType.OBJECT], + default_error_code=BAD_VALUE_ERROR, + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + valid_inputs={ + BsonType.OBJECT: { + "type": "name", + "properties": {"name": "urn:x-mongodb:crs:strictwinding:EPSG:4326"}, + } + }, + ), +] + + +def _build_filter(spec, sample_value): + """Build the $geoIntersects filter based on which keyword is being tested.""" + if spec.keyword == "$geometry": + return {"loc": {"$geoIntersects": {"$geometry": sample_value}}} + if spec.keyword == "type": + geometry = {"type": sample_value, "coordinates": VALID_POLYGON_COORDS} + elif spec.keyword == "crs": + geometry = {"type": "Polygon", "coordinates": VALID_POLYGON_COORDS, "crs": sample_value} + else: + geometry = {"type": "Polygon", "coordinates": sample_value} + return {"loc": {"$geoIntersects": {"$geometry": geometry}}} + + +TEST_CASES = generate_bson_rejection_test_cases(GEOINTERSECTS_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", TEST_CASES) +def test_geoIntersects_bson_type_rejected(collection, bson_type, sample_value, spec): + """Test $geoIntersects rejects invalid BSON types.""" + result = execute_command( + collection, {"find": collection.name, "filter": _build_filter(spec, sample_value)} + ) + assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) + + +ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(GEOINTERSECTS_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) +def test_geoIntersects_bson_type_accepted(collection, bson_type, sample_value, spec): + """Test $geoIntersects accepts valid BSON types.""" + collection.insert_many(spec.expected) + result = execute_command( + collection, {"find": collection.name, "filter": _build_filter(spec, sample_value)} + ) + assertSuccess(result, spec.expected, msg=f"{spec.keyword} should accept {bson_type.value}") diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_core.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_core.py new file mode 100644 index 000000000..97c5e686f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_core.py @@ -0,0 +1,913 @@ +""" +Tests for $geoIntersects core functionality — null/missing field handling, +field type validation, GeoJSON type intersection, valid geometry types, +degenerate geometry, and basic intersection behavior. +""" + +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 + +# Standard query polygon +QUERY_POLYGON = { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]], + } +} + + +NULL_MISSING_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="field_does_not_exist", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + {"_id": 1, "other": "value"}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[{"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Missing field should not match", + ), + QueryTestCase( + id="field_is_null", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + {"_id": 1, "loc": None}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[{"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Null field should not match", + ), +] + + +FIELD_TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="field_is_string", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + {"_id": 1, "loc": "not geo"}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[{"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="String field should not match", + ), + QueryTestCase( + id="field_is_number", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[{"_id": 1, "loc": 42}, {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}], + expected=[{"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Numeric field should not match", + ), + QueryTestCase( + id="field_is_boolean", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[{"_id": 1, "loc": True}, {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}], + expected=[{"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Boolean field should not match", + ), + QueryTestCase( + id="field_is_array_of_geojson", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + { + "_id": 1, + "loc": [ + {"type": "Point", "coordinates": [0, 0]}, + {"type": "Point", "coordinates": [50, 50]}, + ], + }, + {"_id": 2, "loc": [{"type": "Point", "coordinates": [50, 50]}]}, + ], + expected=[ + { + "_id": 1, + "loc": [ + {"type": "Point", "coordinates": [0, 0]}, + {"type": "Point", "coordinates": [50, 50]}, + ], + } + ], + msg="Array of GeoJSON objects should match if any element intersects", + ), +] + + +INTERSECTION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="point_intersects_point_same_location", + filter={"loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [5, 5]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Point query should intersect stored Point at same location", + ), + QueryTestCase( + id="point_intersects_polygon_inside", + filter={"loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}}}, + doc=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + }, + }, + { + "_id": 2, + "loc": { + "type": "Polygon", + "coordinates": [[[20, 20], [30, 20], [30, 30], [20, 30], [20, 20]]], + }, + }, + ], + expected=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + }, + } + ], + msg="Point query should intersect stored Polygon if point inside", + ), + QueryTestCase( + id="linestring_intersects_polygon", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "LineString", "coordinates": [[0, 0], [20, 0]]} + } + } + }, + doc=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + }, + }, + { + "_id": 2, + "loc": { + "type": "Polygon", + "coordinates": [[[20, 20], [30, 20], [30, 30], [20, 30], [20, 20]]], + }, + }, + ], + expected=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + }, + } + ], + msg="LineString query should intersect stored Polygon if line crosses polygon", + ), + QueryTestCase( + id="polygon_intersects_polygon_overlap", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-3, -3], [3, -3], [3, 3], [-3, 3], [-3, -3]]], + } + } + } + }, + doc=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + }, + }, + { + "_id": 2, + "loc": { + "type": "Polygon", + "coordinates": [[[20, 20], [30, 20], [30, 30], [20, 30], [20, 20]]], + }, + }, + ], + expected=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + }, + } + ], + msg="Polygon query should intersect stored Polygon if they overlap", + ), + QueryTestCase( + id="polygon_intersects_point_inside", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Polygon query should intersect stored Point if point inside polygon", + ), + QueryTestCase( + id="multipoint_intersects_polygon", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "MultiPoint", "coordinates": [[0, 0], [50, 50]]} + } + } + }, + doc=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + }, + }, + { + "_id": 2, + "loc": { + "type": "Polygon", + "coordinates": [[[20, 20], [30, 20], [30, 30], [20, 30], [20, 20]]], + }, + }, + ], + expected=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + }, + } + ], + msg="MultiPoint query should intersect if any point inside stored Polygon", + ), + QueryTestCase( + id="polygon_no_intersection", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [50, 50]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [60, 60]}}, + ], + expected=[], + msg="No documents should match when none intersect", + ), + QueryTestCase( + id="geometrycollection_intersects", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "GeometryCollection", + "geometries": [ + {"type": "Point", "coordinates": [0, 0]}, + ], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="GeometryCollection query should intersect if any geometry in collection intersects", + ), + QueryTestCase( + id="multipolygon_intersects_point", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "MultiPolygon", + "coordinates": [ + [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + [[[40, 40], [50, 40], [50, 50], [40, 50], [40, 40]]], + ], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [45, 45]}}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [20, 20]}}, + ], + expected=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [45, 45]}}, + ], + msg="MultiPolygon should intersect if point in any polygon", + ), + QueryTestCase( + id="linestring_crossing_linestring", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "LineString", "coordinates": [[-1, 1], [1, -1]]} + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "LineString", "coordinates": [[-1, -1], [1, 1]]}}, + {"_id": 2, "loc": {"type": "LineString", "coordinates": [[10, 10], [11, 11]]}}, + ], + expected=[ + {"_id": 1, "loc": {"type": "LineString", "coordinates": [[-1, -1], [1, 1]]}}, + ], + msg="Crossing LineStrings should intersect", + ), + QueryTestCase( + id="linestring_parallel_miss", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "LineString", "coordinates": [[0, 2], [1, 2]]} + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "LineString", "coordinates": [[0, 0], [1, 0]]}}, + {"_id": 2, "loc": {"type": "LineString", "coordinates": [[0, 5], [1, 5]]}}, + ], + expected=[], + msg="Parallel non-intersecting LineStrings should not match", + ), + QueryTestCase( + id="linestring_vertex_touching", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "LineString", "coordinates": [[1, 0], [2, 1]]} + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "LineString", "coordinates": [[0, 0], [1, 0]]}}, + {"_id": 2, "loc": {"type": "LineString", "coordinates": [[5, 5], [6, 6]]}}, + ], + expected=[ + {"_id": 1, "loc": {"type": "LineString", "coordinates": [[0, 0], [1, 0]]}}, + ], + msg="LineStrings touching at vertex should intersect", + ), + QueryTestCase( + id="linestring_point_intersection", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "LineString", "coordinates": [[-1, 0], [1, 0]]} + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [5, 5]}}, + ], + expected=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + msg="LineString should intersect Point on the line", + ), + QueryTestCase( + id="tall_polygon", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-1, -60], [1, -60], [1, 60], [-1, 60], [-1, -60]]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 45]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, -45]}}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [50, 0]}}, + ], + expected=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 45]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, -45]}}, + ], + msg="Tall polygon should intersect points within its latitude range", + ), + QueryTestCase( + id="long_equatorial_polygon", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-60, -1], [60, -1], [60, 1], [-60, 1], [-60, -1]]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [50, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [-50, 0]}}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [0, 50]}}, + ], + expected=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [50, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [-50, 0]}}, + ], + msg="Long equatorial polygon should intersect points along equator", + ), +] + +STORED_MULTI_GEOMETRY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="stored_multipoint", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + {"_id": 1, "loc": {"type": "MultiPoint", "coordinates": [[0, 0], [50, 50]]}}, + {"_id": 2, "loc": {"type": "MultiPoint", "coordinates": [[50, 50], [60, 60]]}}, + ], + expected=[{"_id": 1, "loc": {"type": "MultiPoint", "coordinates": [[0, 0], [50, 50]]}}], + msg="Stored MultiPoint should match if any point intersects query polygon", + ), + QueryTestCase( + id="stored_multilinestring", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + { + "_id": 1, + "loc": { + "type": "MultiLineString", + "coordinates": [[[-1, 0], [1, 0]], [[50, 50], [51, 51]]], + }, + }, + { + "_id": 2, + "loc": { + "type": "MultiLineString", + "coordinates": [[[50, 50], [51, 51]], [[60, 60], [61, 61]]], + }, + }, + ], + expected=[ + { + "_id": 1, + "loc": { + "type": "MultiLineString", + "coordinates": [[[-1, 0], [1, 0]], [[50, 50], [51, 51]]], + }, + } + ], + msg="Stored MultiLineString should match if any line intersects query polygon", + ), + QueryTestCase( + id="stored_multipolygon", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + { + "_id": 1, + "loc": { + "type": "MultiPolygon", + "coordinates": [ + [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + [[[40, 40], [50, 40], [50, 50], [40, 50], [40, 40]]], + ], + }, + }, + { + "_id": 2, + "loc": { + "type": "MultiPolygon", + "coordinates": [ + [[[40, 40], [50, 40], [50, 50], [40, 50], [40, 40]]], + [[[60, 60], [70, 60], [70, 70], [60, 70], [60, 60]]], + ], + }, + }, + ], + expected=[ + { + "_id": 1, + "loc": { + "type": "MultiPolygon", + "coordinates": [ + [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + [[[40, 40], [50, 40], [50, 50], [40, 50], [40, 40]]], + ], + }, + } + ], + msg="Stored MultiPolygon should match if any polygon intersects query polygon", + ), + QueryTestCase( + id="stored_geometrycollection", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + { + "_id": 1, + "loc": { + "type": "GeometryCollection", + "geometries": [ + {"type": "Point", "coordinates": [0, 0]}, + {"type": "Point", "coordinates": [50, 50]}, + ], + }, + }, + { + "_id": 2, + "loc": { + "type": "GeometryCollection", + "geometries": [ + {"type": "Point", "coordinates": [50, 50]}, + ], + }, + }, + ], + expected=[ + { + "_id": 1, + "loc": { + "type": "GeometryCollection", + "geometries": [ + {"type": "Point", "coordinates": [0, 0]}, + {"type": "Point", "coordinates": [50, 50]}, + ], + }, + } + ], + msg="Stored GeometryCollection should match if any geometry intersects query polygon", + ), +] + +VALID_GEOMETRY_TYPE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="valid_point", + filter={"loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="$geoIntersects with Point geometry type should be valid", + ), + QueryTestCase( + id="valid_linestring", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "LineString", + "coordinates": [[-1, 0], [1, 0]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="$geoIntersects with LineString geometry type should be valid", + ), + QueryTestCase( + id="valid_polygon", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="$geoIntersects with Polygon geometry type should be valid", + ), + QueryTestCase( + id="valid_multipoint", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "MultiPoint", + "coordinates": [[0, 0], [50, 50]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [20, 20]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="$geoIntersects with MultiPoint geometry type should be valid", + ), + QueryTestCase( + id="valid_multilinestring", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "MultiLineString", + "coordinates": [[[-1, 0], [1, 0]], [[50, 50], [51, 51]]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [20, 20]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="$geoIntersects with MultiLineString geometry type should be valid", + ), + QueryTestCase( + id="valid_multipolygon", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "MultiPolygon", + "coordinates": [ + [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]], + [[[40, 40], [50, 40], [50, 50], [40, 50], [40, 40]]], + ], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [20, 20]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="$geoIntersects with MultiPolygon geometry type should be valid", + ), + QueryTestCase( + id="valid_geometrycollection", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "GeometryCollection", + "geometries": [ + {"type": "Point", "coordinates": [0, 0]}, + { + "type": "LineString", + "coordinates": [[50, 50], [51, 51]], + }, + ], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [20, 20]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="$geoIntersects with GeometryCollection geometry type should be valid", + ), +] + +QUIRKY_BEHAVIOR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="invalid_geometry_array_returns_empty", + filter={"loc": {"$geoIntersects": {"$geometry": [1, 2]}}}, + doc=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + expected=[], + msg="Invalid array $geometry value silently returns no results", + ), + QueryTestCase( + id="invalid_coordinates_as_object_matches", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Point", + "coordinates": {"x": 0, "y": 0}, + } + } + } + }, + doc=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Invalid coordinates as object is treated as valid", + ), + QueryTestCase( + id="extra_fields_in_geometry_ignored", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "Point", "coordinates": [0, 0], "extra": 1} + } + } + }, + doc=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Extra unrecognized fields in $geometry are silently ignored", + ), + QueryTestCase( + id="zero_area_polygon_collinear_matches", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[0, 0], [1, 0], [2, 0], [0, 0]]], + } + } + } + }, + doc=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Zero-area polygon with collinear points does not error and can match", + ), + QueryTestCase( + id="stored_invalid_geojson_skipped", + filter={"loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}}}, + doc=[ + {"_id": 1, "loc": {"type": "Polygon", "coordinates": []}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[{"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Stored invalid GeoJSON is silently skipped without index", + ), +] + +DEGENERATE_GEOMETRY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="polygon_vertex_sharing_no_intersect", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]], + } + } + } + }, + doc=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]], + }, + }, + { + "_id": 2, + "loc": { + "type": "Polygon", + "coordinates": [[[10, 10], [20, 10], [20, 20], [10, 20], [10, 10]]], + }, + }, + ], + expected=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]], + }, + } + ], + msg="Polygons sharing only a vertex do not intersect", + ), + QueryTestCase( + id="polygon_edge_sharing_no_intersect", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]], + } + } + } + }, + doc=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]], + }, + }, + { + "_id": 2, + "loc": { + "type": "Polygon", + "coordinates": [[[10, 0], [20, 0], [20, 10], [10, 10], [10, 0]]], + }, + }, + ], + expected=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]], + }, + } + ], + msg="Polygons sharing only an edge do not intersect", + ), + QueryTestCase( + id="stored_polygon_with_hole_point_inside_hole", + filter={"loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}}}, + doc=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [ + [[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]], + [[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]], + ], + }, + }, + ], + expected=[], + msg="Point inside hole of stored polygon should not intersect", + ), + QueryTestCase( + id="stored_polygon_with_hole_point_in_ring", + filter={"loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [7, 7]}}}}, + doc=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [ + [[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]], + [[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]], + ], + }, + }, + ], + expected=[ + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [ + [[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]], + [[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]], + ], + }, + }, + ], + msg="Point in ring area of stored polygon with hole should intersect", + ), + QueryTestCase( + id="dot_notation_nested_geojson", + filter={ + "geo.loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]], + } + } + } + }, + doc=[ + {"_id": 1, "geo": {"loc": {"type": "Point", "coordinates": [0, 0]}}}, + {"_id": 2, "geo": {"loc": {"type": "Point", "coordinates": [50, 50]}}}, + ], + expected=[{"_id": 1, "geo": {"loc": {"type": "Point", "coordinates": [0, 0]}}}], + msg="$geoIntersects should work with dot notation on nested field", + ), +] + +ALL_TESTS = ( + NULL_MISSING_TESTS + + FIELD_TYPE_TESTS + + INTERSECTION_TESTS + + STORED_MULTI_GEOMETRY_TESTS + + VALID_GEOMETRY_TYPE_TESTS + + QUIRKY_BEHAVIOR_TESTS + + DEGENERATE_GEOMETRY_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_geoIntersects_core(collection, test): + """Test $geoIntersects core functionality and valid geometry types.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_edge_cases.py new file mode 100644 index 000000000..0617955e8 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_edge_cases.py @@ -0,0 +1,424 @@ +""" +Tests for $geoIntersects edge cases — boundary coordinates, precision, +complex geometries, spherical geometry, and big polygon/CRS behavior. +""" + +import pytest +from bson import Decimal128 + +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 CANT_EXTRACT_GEO_KEYS_ERROR +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +EDGE_CASE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="longitude_exactly_neg180", + filter={ + "loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [-180, 0]}}} + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [-180, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [-180, 0]}}], + msg="Longitude exactly -180 should be valid", + ), + QueryTestCase( + id="longitude_exactly_180", + filter={ + "loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [180, 0]}}} + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [180, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [180, 0]}}], + msg="Longitude exactly 180 should be valid", + ), + QueryTestCase( + id="latitude_exactly_neg90", + filter={ + "loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, -90]}}} + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, -90]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, -90]}}], + msg="Latitude exactly -90 should be valid", + ), + QueryTestCase( + id="latitude_exactly_90", + filter={ + "loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 90]}}} + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 90]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 90]}}], + msg="Latitude exactly 90 should be valid", + ), + QueryTestCase( + id="high_precision_coordinates", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Point", + "coordinates": [1.123456789012345, 2.987654321098765], + } + } + } + }, + doc=[ + { + "_id": 1, + "loc": {"type": "Point", "coordinates": [1.123456789012345, 2.987654321098765]}, + }, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[ + { + "_id": 1, + "loc": {"type": "Point", "coordinates": [1.123456789012345, 2.987654321098765]}, + } + ], + msg="High precision coordinates should be valid", + ), + QueryTestCase( + id="integer_coordinates", + filter={"loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [5, 5]}}}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [5, 5]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [5, 5]}}], + msg="Integer coordinates should be valid", + ), + QueryTestCase( + id="polygon_with_hole", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [ + [[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]], + [[-2, -2], [2, -2], [2, 2], [-2, 2], [-2, -2]], + ], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [5, 5]}}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 2, "loc": {"type": "Point", "coordinates": [5, 5]}}], + msg="Polygon with hole should not intersect points in the hole", + ), + QueryTestCase( + id="polygon_many_vertices", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-5, -5], + [-4, -5], + [-3, -5], + [-2, -5], + [-1, -5], + [0, -5], + [1, -5], + [2, -5], + [3, -5], + [4, -5], + [5, -5], + [5, 5], + [-5, 5], + [-5, -5], + ] + ], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Polygon with many vertices should be valid", + ), + QueryTestCase( + id="linestring_many_segments", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "LineString", + "coordinates": [[-5, 0], [-3, 0], [-1, 0], [1, 0], [3, 0], [5, 0]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="LineString with many segments should be valid", + ), + QueryTestCase( + id="linestring_duplicate_points_valid", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "LineString", + "coordinates": [[0, 0], [0, 0], [1, 1]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="LineString with duplicate points should be valid", + ), + QueryTestCase( + id="near_pole", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, 89], [10, 89], [10, 90], [-10, 90], [-10, 89]]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 89.5]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 89.5]}}], + msg="Polygon near pole should intersect point at high latitude", + ), + QueryTestCase( + id="antimeridian", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[179, -1], [-179, -1], [-179, 1], [179, 1], [179, -1]]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [179.5, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [-179.5, 0]}}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [179.5, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [-179.5, 0]}}, + ], + msg="Polygon crossing antimeridian should intersect points on both sides", + ), + QueryTestCase( + id="decimal128_coordinates", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]], + } + } + } + }, + doc=[ + { + "_id": 1, + "loc": {"type": "Point", "coordinates": [Decimal128("1.5"), Decimal128("1.5")]}, + }, + { + "_id": 2, + "loc": {"type": "Point", "coordinates": [Decimal128("50.0"), Decimal128("50.0")]}, + }, + ], + expected=[ + { + "_id": 1, + "loc": {"type": "Point", "coordinates": [Decimal128("1.5"), Decimal128("1.5")]}, + }, + ], + msg="Decimal128 coordinates should be supported", + ), + QueryTestCase( + id="legacy_point_format", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": [1, 1]}, + {"_id": 2, "loc": [50, 50]}, + ], + expected=[{"_id": 1, "loc": [1, 1]}], + msg="Legacy coordinate pair format should be queryable with $geoIntersects", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(EDGE_CASE_TESTS)) +def test_geoIntersects_edge_cases(collection, test): + """Test $geoIntersects 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, msg=test.msg) + + +STRICT_WINDING_CRS = { + "type": "name", + "properties": {"name": "urn:x-mongodb:crs:strictwinding:EPSG:4326"}, +} + +BIG_POLYGON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="small_polygon_default_crs", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [5, 5]}}, + ], + expected=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [5, 5]}}, + ], + msg="Small polygon with default CRS should return intersecting documents", + ), + QueryTestCase( + id="custom_crs_counter_clockwise", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]], + "crs": STRICT_WINDING_CRS, + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Custom CRS with CCW winding should match points inside small polygon", + ), + QueryTestCase( + id="custom_crs_clockwise", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, -10], [-10, 10], [10, 10], [10, -10], [-10, -10]]], + "crs": STRICT_WINDING_CRS, + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}], + msg="Custom CRS with clockwise winding should match complement", + ), + QueryTestCase( + id="complementary_big_polygon_no_crs", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, -10], [-10, 10], [10, 10], [10, -10], [-10, -10]]], + } + } + } + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Clockwise polygon without CRS should be auto-corrected to small polygon", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(BIG_POLYGON_TESTS)) +def test_geoIntersects_big_polygon(collection, test): + """Test $geoIntersects big polygon / CRS handling.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True, msg=test.msg) + + +def test_geoIntersects_big_polygon_index_restriction(collection): + """Test that creating a 2dsphere index on big polygon document fails.""" + collection.insert_one( + { + "_id": 1, + "loc": { + "type": "Polygon", + "coordinates": [[[-10, -10], [-10, 10], [10, 10], [10, -10], [-10, -10]]], + "crs": STRICT_WINDING_CRS, + }, + } + ) + result = execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"loc": "2dsphere"}, "name": "loc_2dsphere"}], + }, + ) + assertFailureCode( + result, + CANT_EXTRACT_GEO_KEYS_ERROR, + msg="Creating 2dsphere index on big polygon should fail", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_errors.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_errors.py new file mode 100644 index 000000000..e333e598a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_errors.py @@ -0,0 +1,681 @@ +""" +Tests for $geoIntersects error cases — invalid arguments, invalid geometry, +coordinate validation, polygon validation, syntax errors, operator combination errors, +NaN/Infinity, CRS type restrictions, and extra operator errors. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + FLOAT_INFINITY, + FLOAT_NAN, + FLOAT_NEGATIVE_INFINITY, +) + +# Standard query polygon for reuse +QUERY_POLYGON = { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]], + } +} + + +INVALID_ARGUMENT_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="no_geometry_operator", + filter={"loc": {"$geoIntersects": {}}}, + error_code=BAD_VALUE_ERROR, + msg="Empty $geoIntersects object without $geometry should error", + ), + QueryTestCase( + id="null_value", + filter={"loc": {"$geoIntersects": None}}, + error_code=BAD_VALUE_ERROR, + msg="Null $geoIntersects value should error", + ), + QueryTestCase( + id="numeric_value", + filter={"loc": {"$geoIntersects": 123}}, + error_code=BAD_VALUE_ERROR, + msg="Numeric $geoIntersects value should error", + ), + QueryTestCase( + id="string_value", + filter={"loc": {"$geoIntersects": "invalid"}}, + error_code=BAD_VALUE_ERROR, + msg="String $geoIntersects value should error", + ), + QueryTestCase( + id="array_value", + filter={"loc": {"$geoIntersects": [1, 2]}}, + error_code=BAD_VALUE_ERROR, + msg="Array $geoIntersects value should error", + ), + QueryTestCase( + id="boolean_value", + filter={"loc": {"$geoIntersects": True}}, + error_code=BAD_VALUE_ERROR, + msg="Boolean $geoIntersects value should error", + ), + QueryTestCase( + id="top_level_geoIntersects_rejected", + filter={"$geoIntersects": QUERY_POLYGON}, + error_code=BAD_VALUE_ERROR, + msg="$geoIntersects as top-level filter should error", + ), +] + + +INVALID_GEOMETRY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="geometry_empty_object", + filter={"loc": {"$geoIntersects": {"$geometry": {}}}}, + error_code=BAD_VALUE_ERROR, + msg="Empty $geometry object should error", + ), + QueryTestCase( + id="geometry_missing_type", + filter={"loc": {"$geoIntersects": {"$geometry": {"coordinates": [[0, 0], [1, 1]]}}}}, + error_code=BAD_VALUE_ERROR, + msg="$geometry missing type field should error", + ), + QueryTestCase( + id="geometry_missing_coordinates", + filter={"loc": {"$geoIntersects": {"$geometry": {"type": "Point"}}}}, + error_code=BAD_VALUE_ERROR, + msg="$geometry missing coordinates field should error", + ), + QueryTestCase( + id="geometry_unknown_type", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "Unknown", "coordinates": [[0, 0], [1, 1]]} + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="$geometry with unknown type should error", + ), + QueryTestCase( + id="geometry_type_feature", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "Feature", "coordinates": [[0, 0], [1, 1]]} + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="$geometry type Feature should error", + ), +] + + +INVALID_COORDINATES_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="coordinates_empty_array_point", + filter={"loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": []}}}}, + error_code=BAD_VALUE_ERROR, + msg="Empty coordinates array for Point should error", + ), + QueryTestCase( + id="coordinates_single_value_point", + filter={"loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0]}}}}, + error_code=BAD_VALUE_ERROR, + msg="Single coordinate for Point should error", + ), + QueryTestCase( + id="coordinates_non_numeric_values", + filter={ + "loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": ["a", "b"]}}} + }, + error_code=BAD_VALUE_ERROR, + msg="Non-numeric coordinate values should error", + ), +] + + +INVALID_GEOMETRY_COLLECTION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="geometrycollection_geometries_null", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "GeometryCollection", "geometries": None}} + } + }, + error_code=BAD_VALUE_ERROR, + msg="GeometryCollection with null geometries should error", + ), + QueryTestCase( + id="geometrycollection_geometries_string", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "GeometryCollection", "geometries": "invalid"} + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="GeometryCollection with string geometries should error", + ), + QueryTestCase( + id="geometrycollection_geometries_number", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "GeometryCollection", "geometries": 123}} + } + }, + error_code=BAD_VALUE_ERROR, + msg="GeometryCollection with numeric geometries should error", + ), + QueryTestCase( + id="geometrycollection_geometries_boolean", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "GeometryCollection", "geometries": True}} + } + }, + error_code=BAD_VALUE_ERROR, + msg="GeometryCollection with boolean geometries should error", + ), + QueryTestCase( + id="geometrycollection_geometries_object", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "GeometryCollection", + "geometries": {"type": "Point", "coordinates": [0, 0]}, + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="GeometryCollection with object geometries (not array) should error", + ), + QueryTestCase( + id="geometrycollection_geometries_empty_array", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "GeometryCollection", "geometries": []}} + } + }, + error_code=BAD_VALUE_ERROR, + msg="GeometryCollection with empty geometries array should error", + ), + QueryTestCase( + id="geometrycollection_invalid_sub_geometry", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "GeometryCollection", + "geometries": [ + {"type": "Point", "coordinates": [999, 999]}, + ], + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="GeometryCollection with invalid sub-geometry coordinates should error", + ), +] + + +COORDINATE_VALIDATION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="longitude_less_than_neg180", + filter={ + "loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [-181, 0]}}} + }, + error_code=BAD_VALUE_ERROR, + msg="Longitude < -180 should error", + ), + QueryTestCase( + id="longitude_greater_than_180", + filter={ + "loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [181, 0]}}} + }, + error_code=BAD_VALUE_ERROR, + msg="Longitude > 180 should error", + ), + QueryTestCase( + id="latitude_less_than_neg90", + filter={ + "loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, -91]}}} + }, + error_code=BAD_VALUE_ERROR, + msg="Latitude < -90 should error", + ), + QueryTestCase( + id="latitude_greater_than_90", + filter={ + "loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 91]}}} + }, + error_code=BAD_VALUE_ERROR, + msg="Latitude > 90 should error", + ), + QueryTestCase( + id="longitude_500_linestring", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "LineString", "coordinates": [[500, 0], [1, 1]]} + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Longitude 500 in LineString should error", + ), + QueryTestCase( + id="latitude_500_linestring", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "LineString", "coordinates": [[0, 500], [1, 1]]} + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Latitude 500 in LineString should error", + ), +] + + +POLYGON_VALIDATION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="unclosed_polygon", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[0, 0], [1, 0], [1, 1], [0, 1]]], + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Unclosed polygon should error", + ), + QueryTestCase( + id="polygon_fewer_than_4_points", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[0, 0], [1, 0], [0, 0]]], + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Polygon with fewer than 4 points should error", + ), + QueryTestCase( + id="self_intersecting_polygon", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[0, 0], [2, 2], [2, 0], [0, 2], [0, 0]]], + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Self-intersecting polygon should error", + ), + QueryTestCase( + id="polygon_latitude_out_of_range", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[0, 91], [1, 91], [1, 92], [0, 92], [0, 91]]], + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Polygon with latitude > 90 should error", + ), + QueryTestCase( + id="polygon_hole_outside_outer_ring", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [ + [[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]], + [[20, 20], [30, 20], [30, 30], [20, 30], [20, 20]], + ], + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Polygon with hole outside outer ring should error", + ), +] + + +SYNTAX_ERROR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="geojson_without_geometry_wrapper", + filter={ + "loc": { + "$geoIntersects": { + "type": "Point", + "coordinates": [0, 0], + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="GeoJSON without $geometry wrapper should error", + ), + QueryTestCase( + id="misspelled_geometry_key", + filter={ + "loc": { + "$geoIntersects": { + "$geomtry": {"type": "LineString", "coordinates": [[0, 0], [1, 1]]} + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Misspelled $geometry key should error", + ), + QueryTestCase( + id="misspelled_coordinates_key", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "LineString", "coordnates": [[0, 0], [1, 1]]} + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Misspelled coordinates key should error", + ), + QueryTestCase( + id="linestring_empty_coordinates", + filter={ + "loc": {"$geoIntersects": {"$geometry": {"type": "LineString", "coordinates": []}}} + }, + error_code=BAD_VALUE_ERROR, + msg="LineString with empty coordinates should error", + ), + QueryTestCase( + id="linestring_single_point", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "LineString", "coordinates": [[0, 0]]}} + } + }, + error_code=BAD_VALUE_ERROR, + msg="LineString with single point should error", + ), + QueryTestCase( + id="point_empty_coordinates", + filter={"loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": []}}}}, + error_code=BAD_VALUE_ERROR, + msg="Point with empty coordinates should error", + ), +] + + +OPERATOR_COMBINATION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="combined_with_near", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}, + "$near": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}, + } + }, + error_code=BAD_VALUE_ERROR, + msg="$geoIntersects combined with $near should error", + ), + QueryTestCase( + id="combined_with_nearSphere", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}, + "$nearSphere": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}, + } + }, + error_code=BAD_VALUE_ERROR, + msg="$geoIntersects combined with $nearSphere should error", + ), + QueryTestCase( + id="extra_geoNear_operator", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}, + "$geoNear": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}, + } + }, + error_code=BAD_VALUE_ERROR, + msg="$geoIntersects with extra $geoNear operator should error", + ), + QueryTestCase( + id="combined_with_geoWithin", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}, + "$geoWithin": { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]], + } + }, + } + }, + error_code=BAD_VALUE_ERROR, + msg="$geoIntersects combined with $geoWithin should error", + ), +] + + +NAN_INF_COORDINATE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="nan_longitude", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [FLOAT_NAN, 0]}} + } + }, + error_code=BAD_VALUE_ERROR, + msg="NaN longitude should error", + ), + QueryTestCase( + id="nan_latitude", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, FLOAT_NAN]}} + } + }, + error_code=BAD_VALUE_ERROR, + msg="NaN latitude should error", + ), + QueryTestCase( + id="inf_longitude", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "Point", "coordinates": [FLOAT_INFINITY, 0]} + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Infinity longitude should error", + ), + QueryTestCase( + id="inf_latitude", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "Point", "coordinates": [0, FLOAT_INFINITY]} + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Infinity latitude should error", + ), + QueryTestCase( + id="neg_inf_longitude", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "Point", "coordinates": [FLOAT_NEGATIVE_INFINITY, 0]} + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Negative infinity longitude should error", + ), + QueryTestCase( + id="neg_inf_latitude", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": {"type": "Point", "coordinates": [0, FLOAT_NEGATIVE_INFINITY]} + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Negative infinity latitude should error", + ), +] + + +CRS_TYPE_RESTRICTION_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="crs_on_point_type", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Point", + "coordinates": [0, 0], + "crs": { + "type": "name", + "properties": {"name": "urn:x-mongodb:crs:strictwinding:EPSG:4326"}, + }, + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Custom CRS on Point type should error (only Polygon supported)", + ), + QueryTestCase( + id="crs_on_linestring_type", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "LineString", + "coordinates": [[0, 0], [1, 1]], + "crs": { + "type": "name", + "properties": {"name": "urn:x-mongodb:crs:strictwinding:EPSG:4326"}, + }, + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Custom CRS on LineString type should error (only Polygon supported)", + ), + QueryTestCase( + id="crs_on_multipoint_type", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "MultiPoint", + "coordinates": [[0, 0], [1, 1]], + "crs": { + "type": "name", + "properties": {"name": "urn:x-mongodb:crs:strictwinding:EPSG:4326"}, + }, + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Custom CRS on MultiPoint type should error (only Polygon supported)", + ), + QueryTestCase( + id="crs_on_multilinestring_type", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "MultiLineString", + "coordinates": [[[0, 0], [1, 1]], [[2, 2], [3, 3]]], + "crs": { + "type": "name", + "properties": {"name": "urn:x-mongodb:crs:strictwinding:EPSG:4326"}, + }, + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Custom CRS on MultiLineString type should error (only Polygon supported)", + ), +] + + +EXTRA_OPERATOR_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="geoIntersects_with_eq_on_same_field", + filter={ + "loc": { + "$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}, + "$eq": {"type": "Point", "coordinates": [0, 0]}, + } + }, + error_code=BAD_VALUE_ERROR, + msg="$geoIntersects with $eq on same field should error", + ), +] + +ALL_ERROR_TESTS = ( + INVALID_ARGUMENT_TESTS + + INVALID_GEOMETRY_TESTS + + INVALID_COORDINATES_TESTS + + INVALID_GEOMETRY_COLLECTION_TESTS + + COORDINATE_VALIDATION_TESTS + + POLYGON_VALIDATION_TESTS + + SYNTAX_ERROR_TESTS + + OPERATOR_COMBINATION_TESTS + + NAN_INF_COORDINATE_TESTS + + CRS_TYPE_RESTRICTION_TESTS + + EXTRA_OPERATOR_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_ERROR_TESTS)) +def test_geoIntersects_errors(collection, test): + """Test $geoIntersects error cases.""" + collection.insert_one({"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_query_context.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_query_context.py new file mode 100644 index 000000000..5c4ac2d03 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_query_context.py @@ -0,0 +1,238 @@ +""" +Tests for $geoIntersects query context and index behavior — find, +$not/$and/$or/$nor combinations, and 2dsphere index scenarios. +""" + +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 + +# Standard query polygon +QUERY_POLYGON = { + "$geometry": { + "type": "Polygon", + "coordinates": [[[-10, -10], [10, -10], [10, 10], [-10, 10], [-10, -10]]], + } +} + +QUERY_CONTEXT_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="find_query", + filter={"loc": {"$geoIntersects": QUERY_POLYGON}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="$geoIntersects in find() query should work", + ), + QueryTestCase( + id="not_negation", + filter={"loc": {"$not": {"$geoIntersects": QUERY_POLYGON}}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}], + msg="$not with $geoIntersects should return non-intersecting documents", + ), + QueryTestCase( + id="and_with_non_geo_condition", + filter={ + "$and": [ + {"loc": {"$geoIntersects": QUERY_POLYGON}}, + {"status": "active"}, + ] + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}, "status": "active"}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}, "status": "inactive"}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [50, 50]}, "status": "active"}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}, "status": "active"}], + msg="$and with $geoIntersects and non-geo condition should work", + ), + QueryTestCase( + id="or_with_geo_conditions", + filter={ + "$or": [ + {"loc": {"$geoIntersects": QUERY_POLYGON}}, + {"status": "special"}, + ] + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}, "status": "normal"}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}, "status": "special"}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [50, 50]}, "status": "normal"}, + ], + expected=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}, "status": "normal"}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}, "status": "special"}, + ], + msg="$or with $geoIntersects should work", + ), + QueryTestCase( + id="and_with_multiple_geoIntersects", + filter={ + "$and": [ + { + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [ + [[-10, -10], [5, -10], [5, 10], [-10, 10], [-10, -10]] + ], + } + } + } + }, + { + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Polygon", + "coordinates": [ + [[-5, -10], [10, -10], [10, 10], [-5, 10], [-5, -10]] + ], + } + } + } + }, + ] + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [-8, 0]}}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [8, 0]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg=( + "$and with multiple $geoIntersects should return only" + " docs in intersection of both regions" + ), + ), + QueryTestCase( + id="nor_with_geoIntersects", + filter={"$nor": [{"loc": {"$geoIntersects": QUERY_POLYGON}}]}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[{"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}], + msg="$nor with $geoIntersects should return non-intersecting documents", + ), + QueryTestCase( + id="or_with_many_points", + filter={ + "$or": [ + { + "loc": { + "$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [0, 0]}} + } + }, + { + "loc": { + "$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [10, 10]}} + } + }, + { + "loc": { + "$geoIntersects": {"$geometry": {"type": "Point", "coordinates": [20, 20]}} + } + }, + ] + }, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [10, 10]}}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [20, 20]}}, + {"_id": 4, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ], + expected=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [10, 10]}}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [20, 20]}}, + ], + msg="$or with multiple $geoIntersects Point queries should work", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(QUERY_CONTEXT_TESTS)) +def test_geoIntersects_query_context(collection, test): + """Test $geoIntersects in various query contexts.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True, msg=test.msg) + + +def test_geoIntersects_basic_2dsphere(collection): + """Test $geoIntersects with basic 2dsphere index.""" + collection.create_index([("loc", "2dsphere")]) + collection.insert_many( + [ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}}, + ] + ) + result = execute_command( + collection, {"find": collection.name, "filter": {"loc": {"$geoIntersects": QUERY_POLYGON}}} + ) + assertSuccess( + result, + [{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="$geoIntersects should work with 2dsphere index", + ) + + +def test_geoIntersects_with_non_geo_filter(collection): + """Test $geoIntersects with non-geo filter on indexed collection.""" + collection.create_index([("loc", "2dsphere")]) + collection.insert_many( + [ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}, "status": "active"}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [1, 1]}, "status": "inactive"}, + {"_id": 3, "loc": {"type": "Point", "coordinates": [50, 50]}, "status": "active"}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoIntersects": QUERY_POLYGON}, "status": "active"}, + }, + ) + assertSuccess( + result, + [{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}, "status": "active"}], + msg="$geoIntersects with non-geo filter on indexed collection should work", + ) + + +def test_geoIntersects_compound_2dsphere(collection): + """Test $geoIntersects with compound 2dsphere index.""" + collection.create_index([("loc", "2dsphere"), ("status", 1)]) + collection.insert_many( + [ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}, "status": "active"}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [50, 50]}, "status": "active"}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoIntersects": QUERY_POLYGON}, "status": "active"}, + }, + ) + assertSuccess( + result, + [{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}, "status": "active"}], + msg="$geoIntersects should work with compound 2dsphere index", + ) diff --git a/documentdb_tests/framework/error_codes.py b/documentdb_tests/framework/error_codes.py index adbc5c20d..d6885cf53 100644 --- a/documentdb_tests/framework/error_codes.py +++ b/documentdb_tests/framework/error_codes.py @@ -80,6 +80,7 @@ MODULO_NON_NUMERIC_ERROR = 16611 MORE_THAN_ONE_DATE_ERROR = 16612 CONCAT_TYPE_ERROR = 16702 +CANT_EXTRACT_GEO_KEYS_ERROR = 16755 HASHED_UNIQUE_NOT_SUPPORTED_ERROR = 16764 EMPTY_VARIABLE_NAME_ERROR = 16867 INVALID_DOLLAR_FIELD_PATH = 16872 From 7cc2961d5138308c98ae664aabadeb79f360ecab Mon Sep 17 00:00:00 2001 From: Victor Tsang Date: Tue, 2 Jun 2026 17:05:54 -0700 Subject: [PATCH 2/2] addressed reviewer comments Signed-off-by: Victor Tsang --- .../geoIntersects/test_geoIntersects_core.py | 88 +++++++++++++++++-- .../test_geoIntersects_errors.py | 67 +++++++++++++- 2 files changed, 148 insertions(+), 7 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_core.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_core.py index 97c5e686f..20ce4ff4d 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_core.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_core.py @@ -1,7 +1,7 @@ """ Tests for $geoIntersects core functionality — null/missing field handling, field type validation, GeoJSON type intersection, valid geometry types, -degenerate geometry, and basic intersection behavior. +degenerate geometry, legacy coordinate parsing, and basic intersection behavior. """ import pytest @@ -684,14 +684,44 @@ QUIRKY_BEHAVIOR_TESTS: list[QueryTestCase] = [ QueryTestCase( - id="invalid_geometry_array_returns_empty", + id="legacy_coordinates_array_match", + filter={"loc": {"$geoIntersects": {"$geometry": [0, 0]}}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [5, 5]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Legacy coordinate array matches point at same location", + ), + QueryTestCase( + id="legacy_coordinates_array_no_match", filter={"loc": {"$geoIntersects": {"$geometry": [1, 2]}}}, doc=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], expected=[], - msg="Invalid array $geometry value silently returns no results", + msg="Legacy coordinate array does not match point at different location", + ), + QueryTestCase( + id="legacy_doc_two_numeric_fields_match", + filter={"loc": {"$geoIntersects": {"$geometry": {"x": 0, "y": 0}}}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [5, 5]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Legacy doc form with 2 numeric fields matches point at same location", + ), + QueryTestCase( + id="legacy_doc_arbitrary_field_names_match", + filter={"loc": {"$geoIntersects": {"$geometry": {"a": 0, "b": 0}}}}, + doc=[ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [5, 5]}}, + ], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Legacy doc form with arbitrary field names matches if values are numeric", ), QueryTestCase( - id="invalid_coordinates_as_object_matches", + id="coordinates_object_two_numeric_fields_matches", filter={ "loc": { "$geoIntersects": { @@ -704,7 +734,55 @@ }, doc=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], - msg="Invalid coordinates as object is treated as valid", + msg="Coordinates object with 2 numeric fields matches", + ), + QueryTestCase( + id="coordinates_object_non_numeric_in_pos3_matches", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Point", + "coordinates": {"x": 0, "y": 0, "z": "str"}, + } + } + } + }, + doc=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Coordinates object with non-numeric in position 3 still matches", + ), + QueryTestCase( + id="coordinates_object_three_numeric_fields_matches", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Point", + "coordinates": {"a": 0, "b": 0, "c": 99}, + } + } + } + }, + doc=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Coordinates object with 3 numeric fields still matches", + ), + QueryTestCase( + id="coordinates_object_numeric_pos1_2_non_numeric_pos3_matches", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Point", + "coordinates": {"a": 0, "b": 0, "c": "str"}, + } + } + } + }, + doc=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + expected=[{"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}], + msg="Coordinates object with numeric in pos 1-2 and non-numeric in pos 3 still matches", ), QueryTestCase( id="extra_fields_in_geometry_ignored", diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_errors.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_errors.py index e333e598a..758f29c41 100644 --- a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/geoIntersects/test_geoIntersects_errors.py @@ -1,7 +1,8 @@ """ Tests for $geoIntersects error cases — invalid arguments, invalid geometry, -coordinate validation, polygon validation, syntax errors, operator combination errors, -NaN/Infinity, CRS type restrictions, and extra operator errors. +coordinate validation, legacy coordinate doc form validation, polygon validation, +syntax errors, operator combination errors, NaN/Infinity, CRS type restrictions, +and extra operator errors. """ import pytest @@ -117,6 +118,30 @@ error_code=BAD_VALUE_ERROR, msg="$geometry type Feature should error", ), + QueryTestCase( + id="legacy_doc_three_fields_with_non_numeric", + filter={"loc": {"$geoIntersects": {"$geometry": {"z": "str", "x": 0, "y": 0}}}}, + error_code=BAD_VALUE_ERROR, + msg="Legacy doc form with 3 fields including non-numeric in first position should error", + ), + QueryTestCase( + id="legacy_doc_three_fields_non_numeric_last", + filter={"loc": {"$geoIntersects": {"$geometry": {"x": 0, "y": 0, "z": "str"}}}}, + error_code=BAD_VALUE_ERROR, + msg="Legacy doc form with 3 fields including non-numeric in last position should error", + ), + QueryTestCase( + id="legacy_doc_three_fields_all_numeric", + filter={"loc": {"$geoIntersects": {"$geometry": {"x": 0, "y": 0, "z": 99}}}}, + error_code=BAD_VALUE_ERROR, + msg="Legacy doc form with 3 numeric fields should error", + ), + QueryTestCase( + id="legacy_doc_single_field", + filter={"loc": {"$geoIntersects": {"$geometry": {"x": 0}}}}, + error_code=BAD_VALUE_ERROR, + msg="Legacy doc form with single field should error", + ), ] @@ -141,6 +166,44 @@ error_code=BAD_VALUE_ERROR, msg="Non-numeric coordinate values should error", ), + QueryTestCase( + id="coordinates_object_non_numeric_in_pos1", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Point", + "coordinates": {"z": "str", "x": 0, "y": 0}, + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Coordinates object with non-numeric in position 1 should error", + ), + QueryTestCase( + id="coordinates_object_non_numeric_first_of_three", + filter={ + "loc": { + "$geoIntersects": { + "$geometry": { + "type": "Point", + "coordinates": {"a": "str", "b": 0, "c": 0}, + } + } + } + }, + error_code=BAD_VALUE_ERROR, + msg="Coordinates object with non-numeric in first position should error", + ), + QueryTestCase( + id="coordinates_object_single_field", + filter={ + "loc": {"$geoIntersects": {"$geometry": {"type": "Point", "coordinates": {"x": 0}}}} + }, + error_code=BAD_VALUE_ERROR, + msg="Coordinates object with single field should error", + ), ]