From 0989bf9605c9b8563a9ae886cfa9320651eb0121 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 19 Jun 2026 13:49:12 -0700 Subject: [PATCH 1/2] Parametrized tests using CommandTestCase, removed out of scope tests and duplicates, edited docStrings Signed-off-by: PatersonProjects --- .../diagnostic/commands/explain/__init__.py | 0 .../test_explain_argument_validation.py | 104 ++++++++ .../explain/test_explain_core_behavior.py | 251 ++++++++++++++++++ .../commands/explain/test_explain_errors.py | 93 +++++++ .../explain/test_explain_geo_queries.py | 98 +++++++ .../explain/test_explain_query_plans.py | 161 +++++++++++ .../test_explain_verbosity_and_response.py | 209 +++++++++++++++ .../explain/test_explain_write_operations.py | 145 ++++++++++ 8 files changed, 1061 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/__init__.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_argument_validation.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_core_behavior.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_errors.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_geo_queries.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_query_plans.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_verbosity_and_response.py create mode 100644 documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_write_operations.py diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/__init__.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_argument_validation.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_argument_validation.py new file mode 100644 index 000000000..f5d965bcb --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_argument_validation.py @@ -0,0 +1,104 @@ +"""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 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, + ) +] +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) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_core_behavior.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_core_behavior.py new file mode 100644 index 000000000..14528171d --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_core_behavior.py @@ -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") diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_errors.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_errors.py new file mode 100644 index 000000000..2474517c3 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_errors.py @@ -0,0 +1,93 @@ +"""Tests for explain command error conditions. + +Covers invalid verbosity strings, invalid and non-explainable explain field +values, and geospatial explain errors (hinting a non-geo index with +$nearSphere). +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode, assertResult +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + COMMAND_NOT_FOUND_ERROR, + ILLEGAL_OPERATION_ERROR, + NO_QUERY_EXECUTION_PLANS_ERROR, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.admin + +EXPLAIN_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + id="invalid_verbosity", + docs=[{"_id": 1, "a": 1}], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "notAMode", + }, + error_code=BAD_VALUE_ERROR, + msg="invalid verbosity string should be BadValue", + ), + CommandTestCase( + id="non_explainable_command", + command=lambda ctx: {"explain": {"insert": ctx.collection, "documents": [{"_id": 1}]}}, + error_code=ILLEGAL_OPERATION_ERROR, + msg="non-explainable command should error", + ), + CommandTestCase( + id="empty_explain_document", + command={"explain": {}}, + error_code=COMMAND_NOT_FOUND_ERROR, + msg="empty explain document should error", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(EXPLAIN_ERROR_TESTS)) +def test_explain_error_cases(collection, test): + """Test explain rejects invalid verbosity, non-explainable commands, + and malformed explain fields. + """ + 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, + ) + + +@pytest.mark.geospatial +def test_explain_find_nearSphere_hint_non_geo_index_fails(collection): + """Test explain of a $nearSphere find hinting a non-geo index returns an error.""" + collection.create_index([("loc", "2dsphere")]) + collection.insert_many( + [ + {"_id": 0, "loc": {"type": "Point", "coordinates": [0, 0]}, "a": 1}, + {"_id": 1, "loc": {"type": "Point", "coordinates": [1, 1]}, "a": 2}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [2, 2]}, "a": 3}, + ] + ) + collection.create_index([("a", 1)]) + near = {"loc": {"$nearSphere": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}}} + result = execute_command( + collection, + { + "explain": {"find": collection.name, "filter": near, "hint": {"a": 1}}, + "verbosity": "queryPlanner", + }, + ) + assertFailureCode( + result, + NO_QUERY_EXECUTION_PLANS_ERROR, + msg="$nearSphere with a non-geo index hint should fail (no viable plan)", + ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_geo_queries.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_geo_queries.py new file mode 100644 index 000000000..0c8a70513 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_geo_queries.py @@ -0,0 +1,98 @@ +"""Tests for explain wiring with geospatial $nearSphere queries. + +These are wiring tests: they verify that explain plans $nearSphere queries +across command types (find, count, distinct, findAndModify). Comprehensive +geospatial parsing and semantics live under the geospatial feature directory; +here we only confirm explain delegates to the geo query path. +""" + +import pytest +from pymongo import IndexModel + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq + +pytestmark = [pytest.mark.admin, pytest.mark.geospatial] + + +NEAR = {"loc": {"$nearSphere": {"$geometry": {"type": "Point", "coordinates": [0, 0]}}}} + +GEO_DOCS = [ + {"_id": 0, "loc": {"type": "Point", "coordinates": [0, 0]}, "a": 1}, + {"_id": 1, "loc": {"type": "Point", "coordinates": [1, 1]}, "a": 2}, + {"_id": 2, "loc": {"type": "Point", "coordinates": [2, 2]}, "a": 3}, +] +GEO_INDEXES = [IndexModel([("loc", "2dsphere")])] + + +NEARSPHERE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + id="find_basic", + docs=GEO_DOCS, + indexes=GEO_INDEXES, + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": NEAR}, + "verbosity": "queryPlanner", + }, + expected={"ok": Eq(1.0)}, + msg="explain find $nearSphere (basic) should plan", + ), + CommandTestCase( + id="count", + docs=GEO_DOCS, + indexes=GEO_INDEXES, + command=lambda ctx: { + "explain": {"count": ctx.collection, "query": NEAR}, + "verbosity": "queryPlanner", + }, + expected={"ok": Eq(1.0)}, + msg="explain count $nearSphere should plan", + ), + CommandTestCase( + id="distinct", + docs=GEO_DOCS, + indexes=GEO_INDEXES, + command=lambda ctx: { + "explain": {"distinct": ctx.collection, "key": "a", "query": NEAR}, + "verbosity": "queryPlanner", + }, + expected={"ok": Eq(1.0)}, + msg="explain distinct $nearSphere should plan", + ), + CommandTestCase( + id="findAndModify", + docs=GEO_DOCS, + indexes=GEO_INDEXES, + command=lambda ctx: { + "explain": { + "findAndModify": ctx.collection, + "query": NEAR, + "update": {"$set": {"visited": True}}, + }, + "verbosity": "queryPlanner", + }, + expected={"ok": Eq(1.0)}, + msg="explain findAndModify $nearSphere should plan", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(NEARSPHERE_TESTS)) +def test_explain_nearSphere_plans(collection, test): + """Test explain plans $nearSphere queries across command types.""" + 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, + ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_query_plans.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_query_plans.py new file mode 100644 index 000000000..78869538f --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_query_plans.py @@ -0,0 +1,161 @@ +"""Tests for explain query-plan and index-usage output. + +Covers collection-scan vs index-scan plan selection, the plan change after an +index is created, aggregate query-planner output, and executionStats counts for +hinted queries and $limit pipelines (including after additional inserts). + +Assertions favor portable signals (nReturned, plan stage presence) over +engine-specific internal plan-node details. +""" + +import pytest +from pymongo import IndexModel + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import ( + assertProperties, + assertResult, +) +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 + + +def test_explain_uses_collscan_without_index(collection): + """Test explain reports COLLSCAN when no usable index exists.""" + collection.insert_many([{"_id": i, "a": i} for i in range(20)]) + result = execute_command(collection, {"explain": {"find": collection.name, "filter": {"a": 5}}}) + assertProperties( + result, + {"queryPlanner.winningPlan.stage": Eq("COLLSCAN")}, + raw_res=True, + msg="plan should be COLLSCAN before index creation", + ) + + +def test_explain_uses_ixscan_after_index_created(collection): + """Test explain switches to IXSCAN after a matching index is created.""" + collection.insert_many([{"_id": i, "a": i} for i in range(20)]) + collection.create_index([("a", 1)]) + result = execute_command(collection, {"explain": {"find": collection.name, "filter": {"a": 5}}}) + assertProperties( + result, + {"queryPlanner.winningPlan.stage": Eq("IXSCAN")}, + raw_res=True, + msg="plan should switch to IXSCAN after index creation", + ) + + +def test_explain_aggregate_contains_query_planner(collection): + """Test explain for an aggregate returns a queryPlanner section.""" + collection.insert_many([{"_id": i, "a": i % 3} for i in range(20)]) + result = execute_command( + collection, + { + "explain": { + "aggregate": collection.name, + "pipeline": [{"$match": {"a": 1}}], + "cursor": {}, + } + }, + ) + assertProperties( + result, + {"queryPlanner": Exists()}, + raw_res=True, + msg="aggregate explain should contain a queryPlanner section", + ) + + +EXEC_STATS_TESTS: list[CommandTestCase] = [ + CommandTestCase( + id="hint_nReturned", + docs=[{"_id": i, "a": i, "b": i % 2} for i in range(10)], + indexes=[IndexModel([("a", 1)])], + command=lambda ctx: { + "explain": { + "find": ctx.collection, + "filter": {"a": {"$gte": 0}}, + "sort": {"a": 1}, + "hint": {"a": 1}, + }, + "verbosity": "executionStats", + }, + expected={"executionStats.nReturned": Eq(10)}, + msg="explain with hint should report correct nReturned", + ), + CommandTestCase( + id="limit_executionStats_nReturned", + docs=[{"_id": i, "a": i} for i in range(20)], + command=lambda ctx: { + "explain": {"aggregate": ctx.collection, "pipeline": [{"$limit": 5}], "cursor": {}}, + "verbosity": "executionStats", + }, + expected={"executionStats.nReturned": Eq(5)}, + msg="executionStats nReturned should match the limit", + ), + CommandTestCase( + id="limit_allPlansExecution_nReturned", + docs=[{"_id": i, "a": i} for i in range(20)], + command=lambda ctx: { + "explain": {"aggregate": ctx.collection, "pipeline": [{"$limit": 5}], "cursor": {}}, + "verbosity": "allPlansExecution", + }, + expected={"executionStats.nReturned": Eq(5)}, + msg="allPlansExecution should honor the limit", + ), + CommandTestCase( + id="limit_multiple_indexes_nReturned", + docs=[{"_id": i, "a": i % 5, "b": i % 3} for i in range(60)], + indexes=[IndexModel([("a", 1)]), IndexModel([("a", 1), ("b", 1)])], + command=lambda ctx: { + "explain": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"a": 2, "b": 1}}, {"$limit": 3}], + "cursor": {}, + }, + "verbosity": "executionStats", + }, + expected={"executionStats.nReturned": Eq(3)}, + msg="limit should be honored with multiple candidate indexes", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(EXEC_STATS_TESTS)) +def test_explain_execution_stats(collection, test): + """Test explain executionStats reports counts matching hints and limits.""" + 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_results_update_after_inserts(collection): + """Test explain executionStats reflects newly inserted documents.""" + collection.insert_many([{"_id": i, "a": i} for i in range(10)]) + collection.create_index([("a", 1)]) + query = { + "explain": {"find": collection.name, "filter": {"a": {"$gte": 0}}, "hint": {"a": 1}}, + "verbosity": "executionStats", + } + execute_command(collection, query) + collection.insert_many([{"_id": i, "a": i} for i in range(10, 15)]) + after = execute_command(collection, query) + assertProperties( + after, + {"executionStats.nReturned": Eq(15)}, + raw_res=True, + msg="explain should reflect additional inserted documents", + ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_verbosity_and_response.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_verbosity_and_response.py new file mode 100644 index 000000000..859a23a19 --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_verbosity_and_response.py @@ -0,0 +1,209 @@ +"""Tests for explain verbosity modes and response structure. + +Covers the response sections produced by each verbosity mode (queryPlanner, +executionStats, allPlansExecution) and the behavioral differences between modes +(query execution and rejected-plan statistics). +""" + +import pytest +from pymongo import IndexModel + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Exists, IsType, NotExists + +pytestmark = pytest.mark.admin + + +RESPONSE_FIELD_TESTS: list[CommandTestCase] = [ + CommandTestCase( + id="queryPlanner_root", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "queryPlanner", + }, + expected={"queryPlanner": Exists()}, + msg="queryPlanner should contain queryPlanner", + ), + CommandTestCase( + id="queryPlanner_parsedQuery", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "queryPlanner", + }, + expected={"queryPlanner.parsedQuery": Exists()}, + msg="queryPlanner should contain queryPlanner.parsedQuery", + ), + CommandTestCase( + id="queryPlanner_winningPlan", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "queryPlanner", + }, + expected={"queryPlanner.winningPlan": Exists()}, + msg="queryPlanner should contain queryPlanner.winningPlan", + ), + CommandTestCase( + id="queryPlanner_namespace", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "queryPlanner", + }, + expected={"queryPlanner.namespace": IsType("string")}, + msg="queryPlanner should contain queryPlanner.namespace", + ), + CommandTestCase( + id="executionStats_root", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "executionStats", + }, + expected={"executionStats": Exists()}, + msg="executionStats should contain executionStats", + ), + CommandTestCase( + id="executionStats_executionSuccess", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "executionStats", + }, + expected={"executionStats.executionSuccess": IsType("bool")}, + msg="executionStats should contain executionStats.executionSuccess", + ), + CommandTestCase( + id="executionStats_nReturned", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "executionStats", + }, + expected={"executionStats.nReturned": IsType("int")}, + msg="executionStats should contain executionStats.nReturned", + ), + CommandTestCase( + id="executionStats_executionTimeMillis", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "executionStats", + }, + expected={"executionStats.executionTimeMillis": Exists()}, + msg="executionStats should contain executionStats.executionTimeMillis", + ), + CommandTestCase( + id="executionStats_totalKeysExamined", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "executionStats", + }, + expected={"executionStats.totalKeysExamined": IsType("int")}, + msg="executionStats should contain executionStats.totalKeysExamined", + ), + CommandTestCase( + id="executionStats_totalDocsExamined", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "executionStats", + }, + expected={"executionStats.totalDocsExamined": IsType("int")}, + msg="executionStats should contain executionStats.totalDocsExamined", + ), + CommandTestCase( + id="allPlans_queryPlanner", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "allPlansExecution", + }, + expected={"queryPlanner": Exists()}, + msg="allPlansExecution should contain queryPlanner", + ), + CommandTestCase( + id="allPlans_executionStats", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "allPlansExecution", + }, + expected={"executionStats": Exists()}, + msg="allPlansExecution should contain executionStats", + ), + CommandTestCase( + id="allPlans_rejectedPlans", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "allPlansExecution", + }, + expected={"queryPlanner.rejectedPlans": IsType("array")}, + msg="allPlansExecution should contain queryPlanner.rejectedPlans", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(RESPONSE_FIELD_TESTS)) +def test_explain_response_contains_field(collection, test): + """Test each verbosity-mode response contains the expected response field.""" + 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, + ) + + +MODE_BEHAVIOR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + id="queryPlanner_omits_executionStats", + docs=[{"_id": i, "a": i % 5} for i in range(20)], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 1}}, + "verbosity": "queryPlanner", + }, + expected={"executionStats": NotExists()}, + msg="queryPlanner must not execute", + ), + CommandTestCase( + id="executionStats_excludes_rejected_plan_stats", + docs=[{"_id": i, "a": i % 5, "b": i % 3} for i in range(60)], + indexes=[IndexModel([("a", 1)]), IndexModel([("a", 1), ("b", 1)])], + command=lambda ctx: { + "explain": {"find": ctx.collection, "filter": {"a": 2, "b": 1}}, + "verbosity": "executionStats", + }, + expected={"executionStats.allPlansExecution": NotExists()}, + msg="executionStats mode should not include rejected-plan execution", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(MODE_BEHAVIOR_TESTS)) +def test_explain_verbosity_mode_behavior(collection, test): + """Test verbosity modes include/exclude execution sections as documented.""" + 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, + ) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_write_operations.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_write_operations.py new file mode 100644 index 000000000..2a44e7b8b --- /dev/null +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_write_operations.py @@ -0,0 +1,145 @@ +"""Tests for explain on write operations. + +Covers that explain does not modify data for update / delete / findAndModify, +and that it reports "would" statistics (nWouldModify, nWouldDelete) and the +DELETE execution stage. +""" + +import pytest +from pymongo import IndexModel + +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 + +pytestmark = pytest.mark.admin + + +def test_explain_update_does_not_modify_documents(collection): + """Test explain on an update does not apply the modification.""" + collection.insert_one({"_id": 1, "a": 1, "b": 1}) + execute_command( + collection, + { + "explain": { + "update": collection.name, + "updates": [{"q": {"a": 1}, "u": {"$set": {"b": 999}}}], + } + }, + ) + after = execute_command(collection, {"find": collection.name, "filter": {"_id": 1}}) + assertSuccess( + after, [{"_id": 1, "a": 1, "b": 1}], msg="explain update must not modify documents" + ) + + +def test_explain_delete_does_not_remove_documents(collection): + """Test explain on a delete does not remove any documents.""" + collection.insert_many([{"_id": i} for i in range(5)]) + execute_command( + collection, {"explain": {"delete": collection.name, "deletes": [{"q": {}, "limit": 0}]}} + ) + after = execute_command(collection, {"find": collection.name, "filter": {}}) + assertSuccess( + after, + [{"_id": i} for i in range(5)], + msg="explain delete must not remove documents", + ignore_doc_order=True, + ) + + +def test_explain_findAndModify_does_not_modify_documents(collection): + """Test explain on a findAndModify does not apply the modification.""" + collection.insert_one({"_id": 1, "a": 1}) + execute_command( + collection, + { + "explain": { + "findAndModify": collection.name, + "query": {"a": 1}, + "update": {"$set": {"a": 2}}, + } + }, + ) + after = execute_command(collection, {"find": collection.name, "filter": {"_id": 1}}) + assertSuccess( + after, [{"_id": 1, "a": 1}], msg="explain findAndModify must not modify documents" + ) + + +WOULD_STATS_TESTS: list[CommandTestCase] = [ + CommandTestCase( + id="update_would_modify_count", + docs=[{"_id": i, "a": i % 2} for i in range(6)], + command=lambda ctx: { + "explain": { + "update": ctx.collection, + "updates": [{"q": {"a": 1}, "u": {"$set": {"b": 9}}, "multi": True}], + }, + "verbosity": "executionStats", + }, + expected={"executionStats.executionStages.nWouldModify": Eq(3)}, + msg="explain update should report nWouldModify", + ), + CommandTestCase( + id="delete_empty_collection_reports_zero", + docs=[], + command=lambda ctx: { + "explain": {"delete": ctx.collection, "deletes": [{"q": {}, "limit": 0}]}, + "verbosity": "executionStats", + }, + expected={"executionStats.executionStages.nWouldDelete": Eq(0)}, + msg="empty collection should report nWouldDelete 0", + ), + CommandTestCase( + id="delete_empty_indexed_collection_reports_zero", + docs=[], + indexes=[IndexModel([("a", 1)], name="a_1")], + command=lambda ctx: { + "explain": {"delete": ctx.collection, "deletes": [{"q": {"a": 1}, "limit": 0}]}, + "verbosity": "executionStats", + }, + expected={"executionStats.executionStages.nWouldDelete": Eq(0)}, + msg="empty indexed collection should report nWouldDelete 0", + ), + CommandTestCase( + id="delete_reports_matching_count", + docs=[{"_id": i, "a": 1 if i < 3 else 2} for i in range(5)], + command=lambda ctx: { + "explain": {"delete": ctx.collection, "deletes": [{"q": {"a": 1}, "limit": 0}]}, + "verbosity": "executionStats", + }, + expected={"executionStats.executionStages.nWouldDelete": Eq(3)}, + msg="should report nWouldDelete matching the query", + ), + CommandTestCase( + id="delete_shows_delete_stage", + docs=[{"_id": i, "a": 1} for i in range(3)], + command=lambda ctx: { + "explain": {"delete": ctx.collection, "deletes": [{"q": {"a": 1}, "limit": 1}]}, + "verbosity": "executionStats", + }, + expected={"executionStats.executionStages.stage": Eq("DELETE")}, + msg="explain single delete should show a DELETE stage", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(WOULD_STATS_TESTS)) +def test_explain_write_would_stats(collection, test): + """Test explain on writes reports would-modify/would-delete stats and stages.""" + 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, + ) From e91fba23fcf4beabcb646616edabbd126ae9b442 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Fri, 19 Jun 2026 15:25:11 -0700 Subject: [PATCH 2/2] Added error override to bson test, Changed test to avoid conflicting case return Signed-off-by: PatersonProjects --- .../explain/test_explain_argument_validation.py | 3 ++- .../commands/explain/test_explain_query_plans.py | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_argument_validation.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_argument_validation.py index f5d965bcb..a4c638efb 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_argument_validation.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_argument_validation.py @@ -16,7 +16,7 @@ generate_bson_acceptance_test_cases, generate_bson_rejection_test_cases, ) -from documentdb_tests.framework.error_codes import TYPE_MISMATCH_ERROR +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 @@ -69,6 +69,7 @@ def test_explain_rejects_non_string_verbosity(collection, bson_type, sample_valu 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) diff --git a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_query_plans.py b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_query_plans.py index 78869538f..a1c338acc 100644 --- a/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_query_plans.py +++ b/documentdb_tests/compatibility/tests/system/diagnostic/commands/explain/test_explain_query_plans.py @@ -21,7 +21,7 @@ ) 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 +from documentdb_tests.framework.property_checks import Eq, Exists, Ne pytestmark = pytest.mark.admin @@ -38,16 +38,16 @@ def test_explain_uses_collscan_without_index(collection): ) -def test_explain_uses_ixscan_after_index_created(collection): - """Test explain switches to IXSCAN after a matching index is created.""" +def test_explain_avoids_collscan_after_index_created(collection): + """Test explain stops using COLLSCAN after a matching index is created.""" collection.insert_many([{"_id": i, "a": i} for i in range(20)]) collection.create_index([("a", 1)]) result = execute_command(collection, {"explain": {"find": collection.name, "filter": {"a": 5}}}) assertProperties( result, - {"queryPlanner.winningPlan.stage": Eq("IXSCAN")}, + {"queryPlanner.winningPlan.stage": Ne("COLLSCAN")}, raw_res=True, - msg="plan should switch to IXSCAN after index creation", + msg="plan should use an index (not COLLSCAN) after index creation", )