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,105 @@
"""Tests for explain command argument validation.

Covers the verbosity parameter (valid string modes and null, rejection of other
BSON types), the explain field (rejection of non-document types), and the
comment parameter (acceptance of any BSON type).
"""

import pytest

from documentdb_tests.framework.assertions import (
assertFailureCode,
assertSuccessPartial,
)
from documentdb_tests.framework.bson_type_validator import (
BsonTypeTestCase,
generate_bson_acceptance_test_cases,
generate_bson_rejection_test_cases,
)
from documentdb_tests.framework.error_codes import MISSING_FIELD_ERROR, TYPE_MISMATCH_ERROR
from documentdb_tests.framework.executor import execute_command
from documentdb_tests.framework.test_constants import BsonType

pytestmark = pytest.mark.admin


VERBOSITY_SPEC = [
BsonTypeTestCase(
id="verbosity",
msg=(
"verbosity should accept string modes and null, "
"and reject other types with TypeMismatch"
),
keyword="verbosity",
valid_types=[BsonType.STRING, BsonType.NULL],
valid_inputs={BsonType.STRING: "queryPlanner"},
default_error_code=TYPE_MISMATCH_ERROR,
)
]
VERBOSITY_ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(VERBOSITY_SPEC)
VERBOSITY_REJECTION_CASES = generate_bson_rejection_test_cases(VERBOSITY_SPEC)


@pytest.mark.parametrize("bson_type,sample_value,spec", VERBOSITY_ACCEPTANCE_CASES)
def test_explain_accepts_valid_verbosity(collection, bson_type, sample_value, spec):
"""Test explain accepts valid verbosity values (string mode and null default)."""
collection.insert_one({"_id": 1, "a": 1})
result = execute_command(
collection,
{"explain": {"find": collection.name, "filter": {"a": 1}}, "verbosity": sample_value},
)
assertSuccessPartial(result, {"ok": 1.0}, msg=spec.msg)


@pytest.mark.parametrize("bson_type,sample_value,spec", VERBOSITY_REJECTION_CASES)
def test_explain_rejects_non_string_verbosity(collection, bson_type, sample_value, spec):
"""Test explain rejects non-string verbosity for every invalid BSON type."""
collection.insert_one({"_id": 1, "a": 1})
result = execute_command(
collection,
{"explain": {"find": collection.name, "filter": {"a": 1}}, "verbosity": sample_value},
)
assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg)


EXPLAIN_ARGUMENT_TYPE_SPEC = [
BsonTypeTestCase(
id="explain",
msg="explain field should reject non-document types with TypeMismatch",
keyword="explain",
valid_types=[BsonType.OBJECT],
default_error_code=TYPE_MISMATCH_ERROR,
error_code_overrides={BsonType.NULL: MISSING_FIELD_ERROR},
)
]
EXPLAIN_ARGUMENT_REJECTION_CASES = generate_bson_rejection_test_cases(EXPLAIN_ARGUMENT_TYPE_SPEC)


@pytest.mark.parametrize("bson_type,sample_value,spec", EXPLAIN_ARGUMENT_REJECTION_CASES)
def test_explain_rejects_non_document_explain_field(collection, bson_type, sample_value, spec):
"""Test explain rejects non-document values for the explain field."""
result = execute_command(
collection,
{"explain": sample_value, "verbosity": "queryPlanner"},
)
assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg)


COMMENT_TYPE_SPEC = [
BsonTypeTestCase(
id="comment",
msg="comment should accept any BSON type",
keyword="comment",
valid_types=list(BsonType),
)
]
COMMENT_ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(COMMENT_TYPE_SPEC)


@pytest.mark.parametrize("bson_type,sample_value,spec", COMMENT_ACCEPTANCE_CASES)
def test_explain_accepts_comment_type(collection, bson_type, sample_value, spec):
"""Test explain accepts a comment of any BSON type at the explain level."""
collection.insert_one({"_id": 1, "a": 1})
cmd = {"explain": {"find": collection.name, "filter": {"a": 1}}, "comment": sample_value}
result = execute_command(collection, cmd)
assertSuccessPartial(result, {"ok": 1.0}, msg=spec.msg)
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
"""Tests for explain command core behavior.

Covers the explainable command surface (find, aggregate, count, distinct,
update, delete, findAndModify), edge cases (empty / non-existent collection,
complex and $expr filters, $lookup sub-pipeline), and plan-cache interaction
(explain does not create plan cache entries).
"""

import pytest

from documentdb_tests.compatibility.tests.core.utils.command_test_case import (
CommandContext,
CommandTestCase,
)
from documentdb_tests.framework.assertions import assertResult, assertSuccess
from documentdb_tests.framework.executor import execute_command
from documentdb_tests.framework.parametrize import pytest_params
from documentdb_tests.framework.property_checks import Eq, Exists

pytestmark = pytest.mark.admin


SUPPORTED_COMMAND_TESTS: list[CommandTestCase] = [
CommandTestCase(
id="find",
docs=[{"_id": i, "a": i, "b": i} for i in range(5)],
command=lambda ctx: {"explain": {"find": ctx.collection, "filter": {"a": 1}}},
expected={"ok": Eq(1.0)},
msg="explain should plan the find command",
),
CommandTestCase(
id="aggregate_single",
docs=[{"_id": i, "a": i, "b": i} for i in range(5)],
command=lambda ctx: {
"explain": {
"aggregate": ctx.collection,
"pipeline": [{"$match": {"a": 1}}],
"cursor": {},
}
},
expected={"ok": Eq(1.0)},
msg="explain should plan the aggregate_single command",
),
CommandTestCase(
id="aggregate_multi",
docs=[{"_id": i, "a": i, "b": i} for i in range(5)],
command=lambda ctx: {
"explain": {
"aggregate": ctx.collection,
"pipeline": [
{"$match": {"a": {"$gt": 0}}},
{"$group": {"_id": None, "c": {"$sum": 1}}},
],
"cursor": {},
}
},
expected={"ok": Eq(1.0)},
msg="explain should plan the aggregate_multi command",
),
CommandTestCase(
id="count",
docs=[{"_id": i, "a": i, "b": i} for i in range(5)],
command=lambda ctx: {"explain": {"count": ctx.collection, "query": {"a": 1}}},
expected={"ok": Eq(1.0)},
msg="explain should plan the count command",
),
CommandTestCase(
id="distinct",
docs=[{"_id": i, "a": i, "b": i} for i in range(5)],
command=lambda ctx: {"explain": {"distinct": ctx.collection, "key": "a"}},
expected={"ok": Eq(1.0)},
msg="explain should plan the distinct command",
),
CommandTestCase(
id="update_single",
docs=[{"_id": i, "a": i, "b": i} for i in range(5)],
command=lambda ctx: {
"explain": {
"update": ctx.collection,
"updates": [{"q": {"a": 1}, "u": {"$set": {"b": 9}}}],
}
},
expected={"ok": Eq(1.0)},
msg="explain should plan the update_single command",
),
CommandTestCase(
id="update_multi",
docs=[{"_id": i, "a": i, "b": i} for i in range(5)],
command=lambda ctx: {
"explain": {
"update": ctx.collection,
"updates": [{"q": {"a": {"$gt": 0}}, "u": {"$set": {"b": 9}}, "multi": True}],
}
},
expected={"ok": Eq(1.0)},
msg="explain should plan the update_multi command",
),
CommandTestCase(
id="delete_single",
docs=[{"_id": i, "a": i, "b": i} for i in range(5)],
command=lambda ctx: {
"explain": {"delete": ctx.collection, "deletes": [{"q": {"a": 1}, "limit": 1}]}
},
expected={"ok": Eq(1.0)},
msg="explain should plan the delete_single command",
),
CommandTestCase(
id="delete_multi",
docs=[{"_id": i, "a": i, "b": i} for i in range(5)],
command=lambda ctx: {
"explain": {
"delete": ctx.collection,
"deletes": [{"q": {"a": {"$gt": 0}}, "limit": 0}],
}
},
expected={"ok": Eq(1.0)},
msg="explain should plan the delete_multi command",
),
CommandTestCase(
id="findAndModify",
docs=[{"_id": i, "a": i, "b": i} for i in range(5)],
command=lambda ctx: {
"explain": {
"findAndModify": ctx.collection,
"query": {"a": 1},
"update": {"$set": {"b": 9}},
}
},
expected={"ok": Eq(1.0)},
msg="explain should plan the findAndModify command",
),
CommandTestCase(
id="find_returns_query_planner",
docs=[{"_id": i, "a": i, "b": i} for i in range(5)],
command=lambda ctx: {"explain": {"find": ctx.collection, "filter": {"a": 1}}},
expected={"queryPlanner": Exists()},
msg="find explain has queryPlanner",
),
]


@pytest.mark.parametrize("test", pytest_params(SUPPORTED_COMMAND_TESTS))
def test_explain_supported_commands(collection, test):
"""Test explain plans each supported command and exposes planner output."""
collection = test.prepare(collection.database, collection)
ctx = CommandContext.from_collection(collection)
result = execute_command(collection, test.build_command(ctx))
assertResult(
result,
expected=test.build_expected(ctx),
error_code=test.error_code,
msg=test.msg,
raw_res=True,
)


EDGE_CASE_TESTS: list[CommandTestCase] = [
CommandTestCase(
id="empty_collection",
docs=[],
command=lambda ctx: {"explain": {"find": ctx.collection, "filter": {"a": 1}}},
expected={"ok": Eq(1.0)},
msg="explain on empty collection should succeed",
),
CommandTestCase(
id="non_existent_collection",
command=lambda ctx: {
"explain": {"find": f"{ctx.collection}_nonexistent", "filter": {"a": 1}}
},
expected={"ok": Eq(1.0)},
msg="explain on non-existent collection should succeed",
),
CommandTestCase(
id="complex_nested_query",
docs=[{"_id": i, "a": i, "b": i % 2} for i in range(10)],
command=lambda ctx: {
"explain": {
"find": ctx.collection,
"filter": {
"$and": [
{"$or": [{"a": {"$lt": 3}}, {"a": {"$gt": 7}}]},
{"$or": [{"b": 0}, {"b": 1}]},
]
},
}
},
expected={"ok": Eq(1.0)},
msg="explain should plan a complex nested query",
),
CommandTestCase(
id="expr_in_filter",
docs=[{"_id": i, "a": i, "b": i + 1} for i in range(5)],
command=lambda ctx: {
"explain": {"find": ctx.collection, "filter": {"$expr": {"$gt": ["$b", "$a"]}}}
},
expected={"ok": Eq(1.0)},
msg="explain should plan an $expr filter",
),
CommandTestCase(
id="aggregate_lookup_subpipeline",
docs=[{"_id": i, "a": i} for i in range(5)],
command=lambda ctx: {
"explain": {
"aggregate": ctx.collection,
"pipeline": [
{
"$lookup": {
"from": ctx.collection,
"let": {"av": "$a"},
"pipeline": [{"$match": {"$expr": {"$eq": ["$a", "$$av"]}}}],
"as": "matches",
}
}
],
"cursor": {},
}
},
expected={"ok": Eq(1.0)},
msg="explain should plan a $lookup sub-pipeline",
),
]


@pytest.mark.parametrize("test", pytest_params(EDGE_CASE_TESTS))
def test_explain_edge_cases(collection, test):
"""Test explain succeeds across collection-state and filter edge cases."""
collection = test.prepare(collection.database, collection)
ctx = CommandContext.from_collection(collection)
result = execute_command(collection, test.build_command(ctx))
assertResult(
result,
expected=test.build_expected(ctx),
error_code=test.error_code,
msg=test.msg,
raw_res=True,
)


def test_explain_does_not_create_plan_cache_entry(collection):
"""Test explain does not create a plan cache entry for the planned query."""
collection.insert_many([{"_id": i, "a": i % 5, "b": i % 3} for i in range(50)])
collection.create_index([("a", 1)])
collection.create_index([("a", 1), ("b", 1)])
collection.database.command({"planCacheClear": collection.name})
execute_command(collection, {"explain": {"find": collection.name, "filter": {"a": 2, "b": 1}}})

cache = execute_command(
collection,
{"aggregate": collection.name, "pipeline": [{"$planCacheStats": {}}], "cursor": {}},
)
assertSuccess(cache, [], msg="explain should not create plan cache entries")
Loading
Loading