diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/__init__.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_argument_handling.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_argument_handling.py new file mode 100644 index 00000000..2dad24ca --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_argument_handling.py @@ -0,0 +1,129 @@ +"""Tests for $center valid argument and type handling.""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import INT64_ZERO + +TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="valid_structure", + filter={"loc": {"$geoWithin": {"$center": [[-74, 40.74], 100]}}}, + doc=[{"_id": 1, "loc": [0, 0]}], + expected=[{"_id": 1, "loc": [0, 0]}], + msg="Should accept valid $center structure", + ), + QueryTestCase( + id="coords_int_and_int64", + filter={"loc": {"$geoWithin": {"$center": [[0, INT64_ZERO], 10]}}}, + doc=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [100, 100]}], + expected=[{"_id": 1, "loc": [0, 0]}], + msg="Should accept int and Int64 mixed coordinates", + ), + QueryTestCase( + id="coords_int64_and_decimal128", + filter={"loc": {"$geoWithin": {"$center": [[Int64(0), Decimal128("0")], 10]}}}, + doc=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [100, 100]}], + expected=[{"_id": 1, "loc": [0, 0]}], + msg="Should accept Int64 and Decimal128 mixed coordinates", + ), + QueryTestCase( + id="coords_float_and_decimal128", + filter={"loc": {"$geoWithin": {"$center": [[0.0, Decimal128("0")], 10]}}}, + doc=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [100, 100]}], + expected=[{"_id": 1, "loc": [0, 0]}], + msg="Should accept float and Decimal128 mixed coordinates", + ), + QueryTestCase( + id="string_field_no_match", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 100]}}}, + doc=[{"_id": 1, "loc": "not a coordinate"}], + expected=[], + msg="Should not match string field value", + ), + QueryTestCase( + id="number_field_no_match", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 100]}}}, + doc=[{"_id": 1, "loc": 42}], + expected=[], + msg="Should not match plain number field value", + ), + QueryTestCase( + id="object_field_no_match", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 100]}}}, + doc=[{"_id": 1, "loc": {"nested": "object"}}], + expected=[], + msg="Should not match nested object field value", + ), + QueryTestCase( + id="boolean_field_no_match", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 100]}}}, + doc=[{"_id": 1, "loc": True}], + expected=[], + msg="Should not match boolean field value", + ), + QueryTestCase( + id="decimal128_field_no_match", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 100]}}}, + doc=[{"_id": 1, "loc": Decimal128("42")}], + expected=[], + msg="Should not match Decimal128 field value", + ), + QueryTestCase( + id="null_field_no_match", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 100]}}}, + doc=[{"_id": 1, "loc": None}, {"_id": 2, "loc": [0, 0]}], + expected=[{"_id": 2, "loc": [0, 0]}], + msg="Should not match document with null location field", + ), + QueryTestCase( + id="missing_field_no_match", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 100]}}}, + doc=[{"_id": 1, "other": "value"}, {"_id": 2, "loc": [0, 0]}], + expected=[{"_id": 2, "loc": [0, 0]}], + msg="Should not match document with missing location field", + ), + QueryTestCase( + id="embedded_xy_document", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + doc=[{"_id": 1, "loc": {"x": 0, "y": 0}}, {"_id": 2, "loc": {"x": 10, "y": 10}}], + expected=[{"_id": 1, "loc": {"x": 0, "y": 0}}], + msg="Should match embedded document with x/y fields", + ), + QueryTestCase( + id="array_more_than_two_elements", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + doc=[{"_id": 1, "loc": [1, 1, 99]}, {"_id": 2, "loc": [10, 10, 0]}], + expected=[{"_id": 1, "loc": [1, 1, 99]}], + msg="Should use first 2 elements of array with more than 2 elements", + ), + QueryTestCase( + id="nested_array_field", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + doc=[{"_id": 1, "loc": [[0, 0]]}, {"_id": 2, "loc": [0, 0]}], + expected=[{"_id": 1, "loc": [[0, 0]]}, {"_id": 2, "loc": [0, 0]}], + msg="Should match array-of-arrays via multi-key expansion", + ), + QueryTestCase( + id="empty_array_no_match", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + doc=[{"_id": 1, "loc": []}, {"_id": 2, "loc": [0, 0]}], + expected=[{"_id": 2, "loc": [0, 0]}], + msg="Should not match document with empty array location field", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TESTS)) +def test_center_argument_handling(collection, test): + """Verifies $center accepts valid argument structures and field formats.""" + if test.doc: + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, msg=test.msg, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_bson_comparison.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_bson_comparison.py new file mode 100644 index 00000000..a62c2bc6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_bson_comparison.py @@ -0,0 +1,114 @@ +""" +Tests for $center BSON type comparison. + +Verifies that $center coordinate and radius values reject invalid BSON types with expected +error codes and accept valid numeric BSON types without error. +""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccess +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR +from documentdb_tests.framework.executor import execute_command + +CENTER_COORDINATE_PARAMS = [ + BsonTypeTestCase( + id="center_coordinate", + msg="$center coordinates should reject non-numeric types", + keyword="$center", + valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL], + default_error_code=BAD_VALUE_ERROR, + valid_inputs={ + BsonType.DOUBLE: 10.5, + BsonType.INT: 10, + BsonType.LONG: Int64(10), + BsonType.DECIMAL: Decimal128("10"), + }, + ), +] + +CENTER_RADIUS_PARAMS = [ + BsonTypeTestCase( + id="center_radius", + msg="$center radius should reject non-numeric types", + keyword="$center", + valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL], + default_error_code=BAD_VALUE_ERROR, + valid_inputs={ + BsonType.DOUBLE: 10.5, + BsonType.INT: 10, + BsonType.LONG: Int64(10), + BsonType.DECIMAL: Decimal128("10"), + }, + ), +] + +COORDINATE_REJECTION_CASES = generate_bson_rejection_test_cases(CENTER_COORDINATE_PARAMS) +RADIUS_REJECTION_CASES = generate_bson_rejection_test_cases(CENTER_RADIUS_PARAMS) + + +@pytest.mark.parametrize( + "bson_type,sample_value,spec", + COORDINATE_REJECTION_CASES, +) +def test_center_coordinate_bson_type_rejected(collection, bson_type, sample_value, spec): + """Test $center rejects invalid BSON types as coordinate values.""" + query = {"loc": {"$geoWithin": {"$center": [[sample_value, sample_value], 10]}}} + result = execute_command(collection, {"find": collection.name, "filter": query}) + assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) + + +@pytest.mark.parametrize( + "bson_type,sample_value,spec", + RADIUS_REJECTION_CASES, +) +def test_center_radius_bson_type_rejected(collection, bson_type, sample_value, spec): + """Test $center rejects invalid BSON types as radius value.""" + query = {"loc": {"$geoWithin": {"$center": [[0, 0], sample_value]}}} + result = execute_command(collection, {"find": collection.name, "filter": query}) + assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) + + +COORDINATE_ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(CENTER_COORDINATE_PARAMS) +RADIUS_ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(CENTER_RADIUS_PARAMS) + + +@pytest.mark.parametrize( + "bson_type,sample_value,spec", + COORDINATE_ACCEPTANCE_CASES, +) +def test_center_coordinate_bson_type_accepted(collection, bson_type, sample_value, spec): + """Test $center accepts valid numeric BSON types as coordinate values.""" + collection.insert_many( + [ + {"_id": 1, "loc": [12, 8]}, + {"_id": 2, "loc": [100, 100]}, + ] + ) + query = {"loc": {"$geoWithin": {"$center": [[sample_value, sample_value], 50]}}} + result = execute_command(collection, {"find": collection.name, "filter": query}) + assertSuccess(result, [{"_id": 1, "loc": [12, 8]}], ignore_doc_order=True) + + +@pytest.mark.parametrize( + "bson_type,sample_value,spec", + RADIUS_ACCEPTANCE_CASES, +) +def test_center_radius_bson_type_accepted(collection, bson_type, sample_value, spec): + """Test $center accepts valid numeric BSON types as radius value.""" + collection.insert_many( + [ + {"_id": 1, "loc": [-30, 25]}, + {"_id": 2, "loc": [100, 100]}, + ] + ) + query = {"loc": {"$geoWithin": {"$center": [[-28, 22], sample_value]}}} + result = execute_command(collection, {"find": collection.name, "filter": query}) + assertSuccess(result, [{"_id": 1, "loc": [-30, 25]}], ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_core_functionality.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_core_functionality.py new file mode 100644 index 00000000..9836638c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_core_functionality.py @@ -0,0 +1,101 @@ +"""Tests for $center core functionality.""" + +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 + +TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="returns_documents_within_circle", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 2]}}}, + doc=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [1, 1]}, {"_id": 3, "loc": [5, 5]}], + expected=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [1, 1]}], + msg="Should return documents within circular bounds", + ), + QueryTestCase( + id="point_at_center_matches", + filter={"loc": {"$geoWithin": {"$center": [[10, 10], 5]}}}, + doc=[{"_id": 1, "loc": [10, 10]}, {"_id": 2, "loc": [20, 20]}], + expected=[{"_id": 1, "loc": [10, 10]}], + msg="Should match point at center (distance=0)", + ), + QueryTestCase( + id="longitude_latitude_ordering", + filter={"loc": {"$geoWithin": {"$center": [[-74, 40], 1]}}}, + doc=[{"_id": 1, "loc": [-74, 40]}, {"_id": 2, "loc": [40, -74]}], + expected=[{"_id": 1, "loc": [-74, 40]}], + msg="Should use [x, y] ordering", + ), + QueryTestCase( + id="includes_geojson_point_without_index", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 10]}}}, + doc=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + expected=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [0, 0]}}, + ], + msg="Should return both legacy and GeoJSON documents without 2d index", + ), + QueryTestCase( + id="no_documents_inside", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 1]}}}, + doc=[{"_id": 1, "loc": [10, 10]}, {"_id": 2, "loc": [20, 20]}], + expected=[], + msg="Should return empty result when no documents match", + ), + QueryTestCase( + id="boundary_points_count", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 1.5]}}}, + doc=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [1, 0]}, + {"_id": 3, "loc": [0, 1]}, + {"_id": 4, "loc": [-1, 0]}, + {"_id": 5, "loc": [0, -1]}, + {"_id": 6, "loc": [2, 2]}, + ], + expected=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [1, 0]}, + {"_id": 3, "loc": [0, 1]}, + {"_id": 4, "loc": [-1, 0]}, + {"_id": 5, "loc": [0, -1]}, + ], + msg="Should return exactly 5 points within radius 1.5", + ), + QueryTestCase( + id="with_additional_field_predicate", + filter={ + "loc": {"$geoWithin": {"$center": [[0, 0], 2]}}, + "type": "a", + }, + doc=[ + {"_id": 1, "loc": [0, 0], "type": "a"}, + {"_id": 2, "loc": [1, 0], "type": "b"}, + {"_id": 3, "loc": [0, 1], "type": "a"}, + {"_id": 4, "loc": [10, 10], "type": "a"}, + ], + expected=[ + {"_id": 1, "loc": [0, 0], "type": "a"}, + {"_id": 3, "loc": [0, 1], "type": "a"}, + ], + msg="Should filter by both $center and additional field predicate", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TESTS)) +def test_center_core_functionality(collection, test): + """Verifies $center returns correct documents within the circular region.""" + if test.doc: + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, msg=test.msg, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_edge_cases.py new file mode 100644 index 00000000..21d83d5e --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_edge_cases.py @@ -0,0 +1,142 @@ +"""Tests for $center edge cases — radius boundaries, coordinate boundaries, and planar distance.""" + +import math + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import FLOAT_INFINITY + +TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="radius_zero", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 0]}}}, + doc=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [0.001, 0]}], + expected=[{"_id": 1, "loc": [0, 0]}], + msg="Should return only exact center point with radius=0", + ), + QueryTestCase( + id="infinity_radius", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], FLOAT_INFINITY]}}}, + doc=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [100, 100]}], + expected=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [100, 100]}], + msg="Should return all documents with Infinity radius", + ), + QueryTestCase( + id="very_small_radius", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 0.0001]}}}, + doc=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [0.00005, 0]}, + {"_id": 3, "loc": [1, 1]}, + ], + expected=[{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [0.00005, 0]}], + msg="Should work with very small radius", + ), + QueryTestCase( + id="negative_coordinates", + filter={"loc": {"$geoWithin": {"$center": [[-100, -100], 10]}}}, + doc=[ + {"_id": 1, "loc": [-100, -100]}, + {"_id": 2, "loc": [-95, -95]}, + {"_id": 3, "loc": [0, 0]}, + ], + expected=[{"_id": 1, "loc": [-100, -100]}, {"_id": 2, "loc": [-95, -95]}], + msg="Should work with negative coordinates", + ), + QueryTestCase( + id="point_at_boundary_included", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 1]}}}, + doc=[{"_id": 1, "loc": [1, 0]}, {"_id": 2, "loc": [2, 0]}], + expected=[{"_id": 1, "loc": [1, 0]}], + msg="Should include point exactly at boundary (distance=radius)", + ), + QueryTestCase( + id="point_just_outside", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 1]}}}, + doc=[{"_id": 1, "loc": [1.001, 0]}], + expected=[], + msg="Should not match point just outside boundary", + ), + QueryTestCase( + id="diagonal_outside", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + doc=[{"_id": 1, "loc": [5, 5]}], + expected=[], + msg="Should exclude point at corner of bounding box (outside circle)", + ), + QueryTestCase( + id="diagonal_on_boundary", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + doc=[ + {"_id": 1, "loc": [5 / math.sqrt(2), 5 / math.sqrt(2)]}, + {"_id": 2, "loc": [4, 4]}, + ], + expected=[{"_id": 1, "loc": [5 / math.sqrt(2), 5 / math.sqrt(2)]}], + msg="Should include point at diagonal boundary", + ), + QueryTestCase( + id="point_on_boundary_of_two_touching_circles", + filter={ + "$and": [ + {"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + {"loc": {"$geoWithin": {"$center": [[10, 0], 5]}}}, + ] + }, + doc=[ + {"_id": 1, "loc": [5, 0]}, + {"_id": 2, "loc": [3, 0]}, + {"_id": 3, "loc": [7, 0]}, + ], + expected=[{"_id": 1, "loc": [5, 0]}], + msg="Should match point exactly on boundary of two tangent circles", + ), + QueryTestCase( + id="very_large_coordinates", + filter={"loc": {"$geoWithin": {"$center": [[1e15, 1e15], 1]}}}, + doc=[ + {"_id": 1, "loc": [1e15, 1e15]}, + {"_id": 2, "loc": [1e15 + 0.5, 1e15]}, + {"_id": 3, "loc": [1e15 + 2, 1e15]}, + ], + expected=[ + {"_id": 1, "loc": [1e15, 1e15]}, + {"_id": 2, "loc": [1e15 + 0.5, 1e15]}, + ], + msg="Should handle very large coordinates without float precision loss", + ), + QueryTestCase( + id="irrational_boundary_sqrt2", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], math.sqrt(2)]}}}, + doc=[ + {"_id": 1, "loc": [1, 1]}, + {"_id": 2, "loc": [1.01, 1.01]}, + ], + expected=[{"_id": 1, "loc": [1, 1]}], + msg="Should include point at irrational boundary (sqrt(2) from origin)", + ), + QueryTestCase( + id="float_rounding_near_boundary", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + doc=[ + {"_id": 1, "loc": [4.9999999999, 0]}, + {"_id": 2, "loc": [5.0000000001, 0]}, + ], + expected=[{"_id": 1, "loc": [4.9999999999, 0]}], + msg="Should distinguish points at near-identical distances around boundary", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TESTS)) +def test_center_edge_cases(collection, test): + """Verifies $center behavior at radius and coordinate boundaries.""" + if test.doc: + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, msg=test.msg, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_errors.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_errors.py new file mode 100644 index 00000000..1ada8241 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_errors.py @@ -0,0 +1,176 @@ +"""Tests for $center error cases — argument structure, context, and numeric edge cases.""" + +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, +) + +TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="empty_array", + filter={"loc": {"$geoWithin": {"$center": []}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject empty array argument", + ), + QueryTestCase( + id="missing_radius", + filter={"loc": {"$geoWithin": {"$center": [[-74, 40.74]]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject missing radius", + ), + QueryTestCase( + id="extra_elements", + filter={"loc": {"$geoWithin": {"$center": [[-74, 40.74], 10, "extra"]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject extra elements", + ), + QueryTestCase( + id="non_array_string", + filter={"loc": {"$geoWithin": {"$center": "invalid"}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject non-array string argument", + ), + QueryTestCase( + id="non_array_number", + filter={"loc": {"$geoWithin": {"$center": 123}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject non-array number argument", + ), + QueryTestCase( + id="null_argument", + filter={"loc": {"$geoWithin": {"$center": None}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject null argument", + ), + QueryTestCase( + id="center_one_coordinate", + filter={"loc": {"$geoWithin": {"$center": [[-74], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject center with only 1 coordinate", + ), + QueryTestCase( + id="center_three_coordinates", + filter={"loc": {"$geoWithin": {"$center": [[-74, 40.74, 0], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject center with 3 coordinates", + ), + QueryTestCase( + id="center_empty_array", + filter={"loc": {"$geoWithin": {"$center": [[], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject center with 0 coordinates", + ), + QueryTestCase( + id="without_geoWithin_wrapper", + filter={"loc": {"$center": [[0, 0], 10]}}, + error_code=BAD_VALUE_ERROR, + msg="Should error when $center used without $geoWithin", + ), + QueryTestCase( + id="with_geoIntersects", + filter={"loc": {"$geoIntersects": {"$center": [[0, 0], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should error when $center used with $geoIntersects", + ), + QueryTestCase( + id="with_near", + filter={"loc": {"$near": {"$center": [[0, 0], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should error when $center used with $near", + ), + QueryTestCase( + id="negative_radius", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], -1]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject negative radius", + ), + QueryTestCase( + id="nan_radius", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], FLOAT_NAN]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject NaN radius", + ), + QueryTestCase( + id="negative_infinity_radius", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], FLOAT_NEGATIVE_INFINITY]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject -Infinity radius", + ), + QueryTestCase( + id="nan_x_coordinate", + filter={"loc": {"$geoWithin": {"$center": [[FLOAT_NAN, 0], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject NaN x-coordinate", + ), + QueryTestCase( + id="infinity_x_coordinate", + filter={"loc": {"$geoWithin": {"$center": [[FLOAT_INFINITY, 0], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject Infinity x-coordinate", + ), + QueryTestCase( + id="string_in_center_coordinates", + filter={"loc": {"$geoWithin": {"$center": [["a", 0], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject string in center coordinates", + ), + QueryTestCase( + id="string_as_radius", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], "10"]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject string as radius", + ), + QueryTestCase( + id="nan_y_coordinate", + filter={"loc": {"$geoWithin": {"$center": [[0, FLOAT_NAN], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject NaN y-coordinate", + ), + QueryTestCase( + id="infinity_y_coordinate", + filter={"loc": {"$geoWithin": {"$center": [[0, FLOAT_INFINITY], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject Infinity y-coordinate", + ), + QueryTestCase( + id="negative_infinity_x_coordinate", + filter={"loc": {"$geoWithin": {"$center": [[FLOAT_NEGATIVE_INFINITY, 0], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject -Infinity x-coordinate", + ), + QueryTestCase( + id="negative_infinity_y_coordinate", + filter={"loc": {"$geoWithin": {"$center": [[0, FLOAT_NEGATIVE_INFINITY], 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject -Infinity y-coordinate", + ), + QueryTestCase( + id="boolean_as_radius", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], True]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject boolean as radius", + ), + QueryTestCase( + id="object_as_center", + filter={"loc": {"$geoWithin": {"$center": [{"x": 0}, 10]}}}, + error_code=BAD_VALUE_ERROR, + msg="Should reject object as center", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TESTS)) +def test_center_errors(collection, test): + """Verifies $center rejects invalid arguments with appropriate error codes.""" + 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/specifiers/center/test_center_index_behavior.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_index_behavior.py new file mode 100644 index 00000000..bbaf3b52 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_index_behavior.py @@ -0,0 +1,209 @@ +"""Tests for $center index behavior — 2d index interaction, index bounds, and configuration.""" + +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command + + +def test_center_with_2d_index_same_results(collection): + """Test $center with 2d index returns same results as without index.""" + collection.insert_many( + [ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [1, 1]}, + {"_id": 3, "loc": [10, 10]}, + ] + ) + collection.create_index([("loc", "2d")]) + result = execute_command( + collection, + {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$center": [[0, 0], 2]}}}}, + ) + assertSuccess( + result, + [{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [1, 1]}], + msg="Should return same results with 2d index", + ) + + +def test_center_with_2d_index_excludes_geojson(collection): + """Test $center with 2d index returns legacy coordinate documents.""" + collection.insert_many( + [ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [1, 1]}, + ] + ) + collection.create_index([("loc", "2d")]) + result = execute_command( + collection, + {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}}, + ) + assertSuccess( + result, + [{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [1, 1]}], + ignore_doc_order=True, + msg="Should return legacy coordinate documents with 2d index", + ) + + +def test_center_with_2d_index_custom_bounds(collection): + """Test $center with 2d index using custom min/max bounds.""" + collection.create_index([("loc", "2d")], min=-500, max=500) + collection.insert_many( + [ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [100, 100]}, + {"_id": 3, "loc": [400, 400]}, + ] + ) + result = execute_command( + collection, + {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$center": [[0, 0], 150]}}}}, + ) + assertSuccess( + result, + [{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [100, 100]}], + msg="Should work with custom 2d index bounds", + ) + + +def test_center_radius_at_full_index_bounds(collection): + """Test $center with radius at full index bounds returns documents.""" + collection.create_index([("loc", "2d")], min=-100, max=100) + collection.insert_many( + [ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [50, 50]}, + {"_id": 3, "loc": [-50, -50]}, + ] + ) + result = execute_command( + collection, + {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$center": [[0, 0], 100]}}}}, + ) + assertSuccess( + result, + [{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [50, 50]}, {"_id": 3, "loc": [-50, -50]}], + ignore_doc_order=True, + msg="Should return documents within radius at full index bounds", + ) + + +def test_center_off_center_large_radius(collection): + """Test $center with off-center point and large radius returns documents.""" + collection.create_index([("loc", "2d")], min=-100, max=100) + collection.insert_many( + [ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [50, 50]}, + {"_id": 3, "loc": [99, 99]}, + ] + ) + result = execute_command( + collection, + {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$center": [[50, 50], 80]}}}}, + ) + assertSuccess( + result, + [{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [50, 50]}, {"_id": 3, "loc": [99, 99]}], + ignore_doc_order=True, + msg="Should return documents with off-center large radius", + ) + + +def test_center_with_2dsphere_index_not_supported(collection): + """Test $center behavior with 2dsphere index — falls back to collection scan.""" + collection.insert_many( + [ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [1, 1]}}, + ] + ) + collection.create_index([("loc", "2dsphere")]) + result = execute_command( + collection, + {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}}, + ) + assertSuccess( + result, + [ + {"_id": 1, "loc": {"type": "Point", "coordinates": [0, 0]}}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [1, 1]}}, + ], + ignore_doc_order=True, + msg="Should work with 2dsphere index (falls back to collection scan)", + ) + + +def test_center_outside_2d_index_bounds(collection): + """Test $center query with center outside 2d index bounds.""" + collection.create_index([("loc", "2d")], min=-100, max=100) + collection.insert_many( + [ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [50, 50]}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$center": [[200, 200], 300]}}}, + }, + ) + assertSuccess( + result, + [{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [50, 50]}], + ignore_doc_order=True, + msg="Should handle center outside 2d index bounds", + ) + + +def test_center_2d_index_mixed_legacy_and_geojson(collection): + """Test $center with 2d index on collection with both legacy and GeoJSON documents.""" + collection.create_index([("loc", "2d")]) + collection.insert_many( + [ + {"_id": 1, "loc": [5, 5]}, + {"_id": 3, "loc": [50, 50]}, + ] + ) + result = execute_command( + collection, + {"find": collection.name, "filter": {"loc": {"$geoWithin": {"$center": [[0, 0], 10]}}}}, + ) + assertSuccess( + result, + [{"_id": 1, "loc": [5, 5]}], + ignore_doc_order=True, + msg="Should match only legacy coordinate docs when 2d index exists", + ) + + +def test_center_compound_2d_index(collection): + """Test $center with compound index {loc: '2d', status: 1}.""" + collection.create_index([("loc", "2d"), ("status", 1)]) + collection.insert_many( + [ + {"_id": 1, "loc": [0, 0], "status": "active"}, + {"_id": 2, "loc": [1, 1], "status": "inactive"}, + {"_id": 3, "loc": [0, 0], "status": "inactive"}, + {"_id": 4, "loc": [50, 50], "status": "active"}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": { + "loc": {"$geoWithin": {"$center": [[0, 0], 5]}}, + "status": "active", + }, + }, + ) + assertSuccess( + result, + [{"_id": 1, "loc": [0, 0], "status": "active"}], + ignore_doc_order=True, + msg="Should work with compound 2d index filtering on both geo and non-geo fields", + ) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_query_interaction.py b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_query_interaction.py new file mode 100644 index 00000000..c42f078b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/geospatial/specifiers/center/test_center_query_interaction.py @@ -0,0 +1,239 @@ +"""Tests for $center query interaction — combination with other operators, field paths, +and comparison with $centerSphere.""" + +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 + +TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="two_geoWithin_same_field", + filter={ + "$and": [ + {"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + {"loc": {"$geoWithin": {"$center": [[1, 1], 5]}}}, + ] + }, + doc=[{"_id": 1, "loc": [0, 0]}], + expected=[{"_id": 1, "loc": [0, 0]}], + msg="Should handle two $geoWithin on same field", + ), + QueryTestCase( + id="combined_with_equality", + filter={ + "loc": {"$geoWithin": {"$center": [[0, 0], 2]}}, + "status": "active", + }, + doc=[ + {"_id": 1, "loc": [0, 0], "status": "active"}, + {"_id": 2, "loc": [1, 0], "status": "inactive"}, + {"_id": 3, "loc": [0, 1], "status": "active"}, + {"_id": 4, "loc": [10, 10], "status": "active"}, + ], + expected=[ + {"_id": 1, "loc": [0, 0], "status": "active"}, + {"_id": 3, "loc": [0, 1], "status": "active"}, + ], + msg="Should combine $center with equality filter", + ), + QueryTestCase( + id="combined_with_or", + filter={ + "$or": [ + {"loc": {"$geoWithin": {"$center": [[0, 0], 2]}}}, + {"type": "a"}, + ] + }, + doc=[ + {"_id": 1, "loc": [0, 0], "type": "a"}, + {"_id": 2, "loc": [1, 0], "type": "b"}, + {"_id": 3, "loc": [10, 10], "type": "a"}, + ], + expected=[ + {"_id": 1, "loc": [0, 0], "type": "a"}, + {"_id": 2, "loc": [1, 0], "type": "b"}, + {"_id": 3, "loc": [10, 10], "type": "a"}, + ], + msg="Should combine $center with $or operator", + ), + QueryTestCase( + id="nested_field", + filter={"address.loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + doc=[ + {"_id": 1, "address": {"loc": [0, 0]}}, + {"_id": 2, "address": {"loc": [10, 10]}}, + ], + expected=[{"_id": 1, "address": {"loc": [0, 0]}}], + msg="Should work on nested field path", + ), + QueryTestCase( + id="array_of_points", + filter={"locs": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + doc=[ + {"_id": 1, "locs": [[0, 0], [10, 10]]}, + {"_id": 2, "locs": [[20, 20], [30, 30]]}, + ], + expected=[{"_id": 1, "locs": [[0, 0], [10, 10]]}], + msg="Should match if ANY point in array is within circle", + ), + QueryTestCase( + id="uses_planar_geometry", + filter={"loc": {"$geoWithin": {"$center": [[0, 0], 0.15]}}}, + doc=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [0.1, 0]}, + {"_id": 3, "loc": [0, 0.1]}, + ], + expected=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [0.1, 0]}, + {"_id": 3, "loc": [0, 0.1]}, + ], + msg="$center should use planar geometry and return points within Euclidean distance", + ), + QueryTestCase( + id="combined_with_not", + filter={"loc": {"$not": {"$geoWithin": {"$center": [[0, 0], 2]}}}}, + doc=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [1, 1]}, + {"_id": 3, "loc": [10, 10]}, + ], + expected=[{"_id": 3, "loc": [10, 10]}], + msg="Should return documents outside circle when using $not", + ), + QueryTestCase( + id="combined_with_nor", + filter={ + "$nor": [ + {"loc": {"$geoWithin": {"$center": [[0, 0], 2]}}}, + ] + }, + doc=[ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [5, 5]}, + ], + expected=[{"_id": 2, "loc": [5, 5]}], + msg="Should return documents not matching any $nor condition", + ), + QueryTestCase( + id="planar_vs_spherical_divergence", + filter={"loc": {"$geoWithin": {"$center": [[0, 89], 10]}}}, + doc=[ + {"_id": 1, "loc": [10, 89]}, + {"_id": 2, "loc": [0, 79]}, + {"_id": 3, "loc": [11, 89]}, + ], + expected=[{"_id": 1, "loc": [10, 89]}, {"_id": 2, "loc": [0, 79]}], + msg="$center uses planar distance — points at 10 units match regardless of latitude", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(TESTS)) +def test_center_query_interaction(collection, test): + """Verifies $center works correctly combined with other query operators.""" + if test.doc: + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, msg=test.msg, ignore_doc_order=True) + + +def test_center_with_projection(collection): + """Test $center with projection.""" + collection.insert_many( + [ + {"_id": 1, "loc": [0, 0], "name": "A"}, + {"_id": 2, "loc": [10, 10], "name": "B"}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + "projection": {"name": 1}, + }, + ) + assertSuccess(result, [{"_id": 1, "name": "A"}], msg="Should work with projection") + + +def test_center_with_limit(collection): + """Test $center with limit.""" + collection.insert_many( + [ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [1, 0]}, + {"_id": 3, "loc": [0, 1]}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + "limit": 2, + }, + ) + assertSuccess( + result, + [{"_id": 1, "loc": [0, 0]}, {"_id": 2, "loc": [1, 0]}], + msg="Should respect limit", + ) + + +def test_center_with_sort(collection): + """Test $center with sort on a non-geo field.""" + collection.insert_many( + [ + {"_id": 1, "loc": [0, 0], "val": 3}, + {"_id": 2, "loc": [1, 0], "val": 1}, + {"_id": 3, "loc": [0, 1], "val": 2}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + "sort": {"val": 1}, + }, + ) + assertSuccess( + result, + [ + {"_id": 2, "loc": [1, 0], "val": 1}, + {"_id": 3, "loc": [0, 1], "val": 2}, + {"_id": 1, "loc": [0, 0], "val": 3}, + ], + msg="Should respect sort on non-geo field", + ) + + +def test_center_with_skip(collection): + """Test $center with skip.""" + collection.insert_many( + [ + {"_id": 1, "loc": [0, 0]}, + {"_id": 2, "loc": [1, 0]}, + {"_id": 3, "loc": [0, 1]}, + ] + ) + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"loc": {"$geoWithin": {"$center": [[0, 0], 5]}}}, + "skip": 1, + }, + ) + assertSuccess( + result, + [{"_id": 2, "loc": [1, 0]}, {"_id": 3, "loc": [0, 1]}], + msg="Should respect skip", + )