Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pytest


@pytest.fixture
def geo_2dsphere(collection):
"""Create a 2dsphere index on loc."""
collection.create_index([("loc", "2dsphere")])


@pytest.fixture
def geo_2d(collection):
"""Create a 2d index on loc."""
collection.create_index([("loc", "2d")])
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Tests for $maxDistance BSON type validation — GeoJSON and legacy syntax.

The legacy code path returns a different error code (16895) than GeoJSON (BAD_VALUE_ERROR = 2).
"""

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,
LEGACY_MAX_DISTANCE_NOT_NUMBER_ERROR,
)
from documentdb_tests.framework.executor import execute_command

ORIGIN = {"type": "Point", "coordinates": [0, 0]}

# --- GeoJSON (2dsphere index) ---

GEOJSON_BSON_PARAMS = [
BsonTypeTestCase(
id="maxDistance",
msg="$maxDistance should reject non-numeric types",
keyword="$maxDistance",
valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL],
default_error_code=BAD_VALUE_ERROR,
),
]

GEOJSON_REJECTION = generate_bson_rejection_test_cases(GEOJSON_BSON_PARAMS)
GEOJSON_ACCEPTANCE = generate_bson_acceptance_test_cases(GEOJSON_BSON_PARAMS)


@pytest.mark.usefixtures("geo_2dsphere")
@pytest.mark.parametrize("operator", ["$near", "$nearSphere"])
@pytest.mark.parametrize("bson_type,sample_value,spec", GEOJSON_REJECTION)
def test_maxDistance_geojson_bson_rejected(collection, operator, bson_type, sample_value, spec):
"""Verifies $maxDistance rejects invalid BSON types (GeoJSON)."""
filt = {
"loc": {
operator: {
"$geometry": ORIGIN,
spec.keyword: sample_value,
}
}
}
result = execute_command(collection, {"find": collection.name, "filter": filt})
assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg)


@pytest.mark.usefixtures("geo_2dsphere")
@pytest.mark.parametrize("operator", ["$near", "$nearSphere"])
@pytest.mark.parametrize("bson_type,sample_value,spec", GEOJSON_ACCEPTANCE)
def test_maxDistance_geojson_bson_accepted(collection, operator, bson_type, sample_value, spec):
"""Verifies $maxDistance accepts valid BSON types (GeoJSON)."""
collection.insert_one({"_id": 1, "loc": ORIGIN})
filt = {
"loc": {
operator: {
"$geometry": ORIGIN,
spec.keyword: sample_value,
}
}
}
result = execute_command(collection, {"find": collection.name, "filter": filt})
assertSuccess(
result,
[{"_id": 1, "loc": ORIGIN}],
msg=f"{spec.keyword} should accept {bson_type.value}",
)


# --- Legacy (2d index) ---

LEGACY_BSON_PARAMS = [
BsonTypeTestCase(
id="maxDistance_legacy",
msg="$maxDistance should reject non-numeric types (legacy)",
keyword="$maxDistance",
valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL],
default_error_code=LEGACY_MAX_DISTANCE_NOT_NUMBER_ERROR,
),
]

LEGACY_REJECTION = generate_bson_rejection_test_cases(LEGACY_BSON_PARAMS)
LEGACY_ACCEPTANCE = generate_bson_acceptance_test_cases(LEGACY_BSON_PARAMS)


@pytest.mark.usefixtures("geo_2d")
@pytest.mark.parametrize("operator", ["$near", "$nearSphere"])
@pytest.mark.parametrize("bson_type,sample_value,spec", LEGACY_REJECTION)
def test_maxDistance_legacy_bson_rejected(collection, operator, bson_type, sample_value, spec):
"""Verifies $maxDistance rejects invalid BSON types (legacy)."""
filt = {"loc": {operator: [0, 0], spec.keyword: sample_value}}
result = execute_command(collection, {"find": collection.name, "filter": filt})
assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg)


@pytest.mark.usefixtures("geo_2d")
@pytest.mark.parametrize("operator", ["$near", "$nearSphere"])
@pytest.mark.parametrize("bson_type,sample_value,spec", LEGACY_ACCEPTANCE)
def test_maxDistance_legacy_bson_accepted(collection, operator, bson_type, sample_value, spec):
"""Verifies $maxDistance accepts valid BSON types (legacy)."""
collection.insert_one({"_id": 1, "loc": [0, 0]})
filt = {"loc": {operator: [0, 0], spec.keyword: sample_value}}
result = execute_command(collection, {"find": collection.name, "filter": filt})
assertSuccess(
result,
[{"_id": 1, "loc": [0, 0]}],
msg=f"{spec.keyword} should accept {bson_type.value} (legacy)",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""Tests for $maxDistance error cases — invalid context, values, types, and missing index."""

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 assertFailureCode
from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR, NO_QUERY_EXECUTION_PLANS_ERROR
from documentdb_tests.framework.executor import execute_command
from documentdb_tests.framework.parametrize import pytest_params
from documentdb_tests.framework.test_constants import (
DECIMAL128_INFINITY,
DECIMAL128_MAX,
DECIMAL128_NAN,
DECIMAL128_NEGATIVE_INFINITY,
FLOAT_INFINITY,
FLOAT_NAN,
FLOAT_NEGATIVE_INFINITY,
)

ORIGIN = {"type": "Point", "coordinates": [0, 0]}


ERROR_TESTS: list[QueryTestCase] = [
# Invalid context
QueryTestCase(
id="without_near_context",
filter={"loc": {"$maxDistance": 1000}},
error_code=BAD_VALUE_ERROR,
msg="Should reject $maxDistance without $near or $nearSphere context",
),
QueryTestCase(
id="with_geoWithin",
filter={
"loc": {
"$geoWithin": {
"$geometry": {
"type": "Polygon",
"coordinates": [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]],
}
},
"$maxDistance": 1000,
}
},
error_code=BAD_VALUE_ERROR,
msg="Should reject $maxDistance with $geoWithin",
),
QueryTestCase(
id="nearSphere_without_geometry",
filter={"loc": {"$nearSphere": {"$maxDistance": 5000, "$minDistance": 0}}},
error_code=BAD_VALUE_ERROR,
msg="Should reject $nearSphere with $maxDistance but no $geometry",
),
# Invalid values
QueryTestCase(
id="negative",
filter={"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": -1}}},
error_code=BAD_VALUE_ERROR,
msg="Should reject negative $maxDistance",
),
QueryTestCase(
id="negative_double",
filter={"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": -0.5}}},
error_code=BAD_VALUE_ERROR,
msg="Should reject negative double $maxDistance",
),
QueryTestCase(
id="negative_int64",
filter={"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": Int64(-1)}}},
error_code=BAD_VALUE_ERROR,
msg="Should reject negative Int64 $maxDistance",
),
QueryTestCase(
id="negative_decimal128",
filter={"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": Decimal128("-1")}}},
error_code=BAD_VALUE_ERROR,
msg="Should reject negative Decimal128 $maxDistance",
),
QueryTestCase(
id="nan",
filter={"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": FLOAT_NAN}}},
error_code=BAD_VALUE_ERROR,
msg="Should reject NaN $maxDistance",
),
QueryTestCase(
id="infinity",
filter={"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": FLOAT_INFINITY}}},
error_code=BAD_VALUE_ERROR,
msg="Should reject Infinity $maxDistance",
),
QueryTestCase(
id="negative_infinity",
filter={"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": FLOAT_NEGATIVE_INFINITY}}},
error_code=BAD_VALUE_ERROR,
msg="Should reject -Infinity $maxDistance",
),
QueryTestCase(
id="decimal128_max",
filter={"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": DECIMAL128_MAX}}},
error_code=BAD_VALUE_ERROR,
msg="Should reject $maxDistance=DECIMAL128_MAX as non-negative overflow",
),
QueryTestCase(
id="decimal128_nan",
filter={"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": DECIMAL128_NAN}}},
error_code=BAD_VALUE_ERROR,
msg="Should reject Decimal128 NaN $maxDistance",
),
QueryTestCase(
id="decimal128_infinity",
filter={"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": DECIMAL128_INFINITY}}},
error_code=BAD_VALUE_ERROR,
msg="Should reject Decimal128 Infinity $maxDistance",
),
QueryTestCase(
id="decimal128_negative_infinity",
filter={
"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": DECIMAL128_NEGATIVE_INFINITY}}
},
error_code=BAD_VALUE_ERROR,
msg="Should reject Decimal128 -Infinity $maxDistance",
),
]


@pytest.mark.usefixtures("geo_2dsphere")
@pytest.mark.parametrize("test", pytest_params(ERROR_TESTS))
def test_maxDistance_errors(collection, test):
"""Verifies $maxDistance rejects invalid contexts, values, and types."""
result = execute_command(collection, {"find": collection.name, "filter": test.filter})
assertFailureCode(result, test.error_code, msg=test.msg)


# No geo_2dsphere fixture applied — these tests intentionally run without an index.
NO_INDEX_TESTS: list[QueryTestCase] = [
QueryTestCase(
id="near_without_index",
filter={"loc": {"$near": {"$geometry": ORIGIN, "$maxDistance": 1000}}},
error_code=NO_QUERY_EXECUTION_PLANS_ERROR,
msg="Should error when no geospatial index exists",
),
QueryTestCase(
id="nearSphere_without_index",
filter={"loc": {"$nearSphere": {"$geometry": ORIGIN, "$maxDistance": 1000}}},
error_code=NO_QUERY_EXECUTION_PLANS_ERROR,
msg="Should error when no geospatial index for $nearSphere",
),
]


@pytest.mark.parametrize("test", pytest_params(NO_INDEX_TESTS))
def test_maxDistance_no_index_errors(collection, test):
"""Verifies $maxDistance fails without geospatial index."""
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)
Loading
Loading