diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/__init__.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_behavior.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_behavior.py new file mode 100644 index 000000000..e98715a80 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_behavior.py @@ -0,0 +1,584 @@ +"""Tests for removeQuerySettings command behavioral verification. + +Verifies that removeQuerySettings actually removes query settings from the +cluster, not just that it returns ok: 1.0. Uses $querySettings to observe +settings state before and after removal. +""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from documentdb_tests.compatibility.tests.core.query_planning.utils.settings_test_case import ( + SettingsTestCase, +) +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, +) +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.requires(cluster_admin=True), pytest.mark.no_parallel] + +# Property [Remove By Query Shape]: removeQuerySettings removes settings +# when given the original query shape, verified via $querySettings. +# Property [Remove By Hash]: removeQuerySettings removes settings when given +# the query shape hash string, verified via $querySettings. +# Property [Remove Distinct Shape]: removeQuerySettings removes settings for +# distinct query shapes, verified via $querySettings. +# Property [Remove Aggregate Shape]: removeQuerySettings removes settings for +# aggregate query shapes, verified via $querySettings. +# Property [Shape Matching Ignores Filter Values]: query shape matching uses +# field structure, not values. Removing with different filter values removes +# the original setting. +REMOVEQUERYSETTINGS_SETTING_REMOVED_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( + "removes_by_query_shape", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"r1": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + } + ], + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"r1": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"r1": 1}, + "$db": ctx.database, + } + } + ], + msg="removeQuerySettings should remove the setting by query shape", + ), + SettingsTestCase( + "removes_by_hash", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"r2": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + } + ], + command=lambda ctx: {"removeQuerySettings": ctx.setup_results[0]["queryShapeHash"]}, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"r2": 1}, + "$db": ctx.database, + } + } + ], + msg="removeQuerySettings should remove the setting by hash", + ), + SettingsTestCase( + "removes_distinct_shape", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + } + ], + command=lambda ctx: { + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "$db": ctx.database, + } + } + ], + msg="removeQuerySettings should remove the distinct setting", + ), + SettingsTestCase( + "removes_aggregate_shape", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"x": 1}}], + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + } + ], + command=lambda ctx: { + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"x": 1}}], + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"x": 1}}], + "$db": ctx.database, + } + } + ], + msg="removeQuerySettings should remove the aggregate setting", + ), + SettingsTestCase( + "shape_ignores_filter_values", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"sm1": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + } + ], + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"sm1": 999}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"sm1": 1}, + "$db": ctx.database, + } + } + ], + msg="shape matching should ignore filter values and remove the setting", + ), +] + + +@pytest.mark.admin +@pytest.mark.parametrize("test", pytest_params(REMOVEQUERYSETTINGS_SETTING_REMOVED_TESTS)) +def test_removeQuerySettings_setting_removed(collection, test): + """Test that removeQuerySettings actually removes settings, verified via $querySettings.""" + ctx = CommandContext.from_collection(collection) + try: + for cmd in test.build_setup(ctx): + r = execute_admin_command(collection, cmd) + ctx.setup_results.append(r) + expected_hash = ctx.setup_results[0]["queryShapeHash"] + + execute_admin_command(collection, test.build_command(ctx)) + + admin = collection.database.client.admin + qs_result = admin.command( + {"aggregate": 1, "pipeline": [{"$querySettings": {}}], "cursor": {}} + ) + batch: list[dict[str, Any]] = qs_result.get("cursor", {}).get("firstBatch", []) + count = sum(1 for s in batch if s.get("queryShapeHash") == expected_hash) + assertSuccessPartial( + {"count": count}, + {"count": 0}, + msg=test.msg, + ) + finally: + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass + + +# Property [Idempotent Removal]: calling removeQuerySettings a second time +# for the same query shape succeeds silently without error. +REMOVEQUERYSETTINGS_IDEMPOTENT_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( + "idempotent_removal", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"r3": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"r3": 1}, + "$db": ctx.database, + } + }, + ], + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"r3": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"r3": 1}, + "$db": ctx.database, + } + } + ], + msg="removeQuerySettings should succeed silently on second removal", + ), +] + + +@pytest.mark.admin +@pytest.mark.parametrize("test", pytest_params(REMOVEQUERYSETTINGS_IDEMPOTENT_TESTS)) +def test_removeQuerySettings_idempotent(collection, test): + """Test removeQuerySettings is idempotent on second call.""" + ctx = CommandContext.from_collection(collection) + try: + for cmd in test.build_setup(ctx): + r = execute_admin_command(collection, cmd) + ctx.setup_results.append(r) + result = execute_admin_command(collection, test.build_command(ctx)) + assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) + finally: + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass + + +# Property [Shape Matching Includes Collection]: collection name is part of +# the query shape. Removing with a different collection does not affect the +# original setting. +# Property [Shape Matching Includes $db]: $db is part of the query shape. +# Removing with a different $db does not affect the original setting. +# Property [Shape Matching Includes Sort Direction]: sort direction is part +# of the query shape. Removing with a different sort direction does not +# affect the original setting. +# Property [Shape Matching Includes Extra Fields]: adding extra fields +# changes the query shape. Removing with extra fields does not affect the +# original filter-only setting. +REMOVEQUERYSETTINGS_SHAPE_PERSISTS_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( + "shape_collection_name_matters", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"sm2": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + } + ], + command=lambda ctx: { + "removeQuerySettings": { + "find": "other_collection", + "filter": {"sm2": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"sm2": 1}, + "$db": ctx.database, + } + } + ], + msg="removing with different collection should not affect original setting", + ), + SettingsTestCase( + "shape_db_matters", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"sm3": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + } + ], + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"sm3": 1}, + "$db": "other_database", + } + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"sm3": 1}, + "$db": ctx.database, + } + } + ], + msg="removing with different $db should not affect original setting", + ), + SettingsTestCase( + "shape_sort_direction_matters", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"sm4": 1}, + "sort": {"sm4": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + } + ], + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"sm4": 1}, + "sort": {"sm4": -1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"sm4": 1}, + "sort": {"sm4": 1}, + "$db": ctx.database, + } + } + ], + msg="removing with different sort direction should not affect original setting", + ), + SettingsTestCase( + "shape_extra_fields_change_shape", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"sm5": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + } + ], + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"sm5": 1}, + "sort": {"sm5": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"sm5": 1}, + "$db": ctx.database, + } + } + ], + msg="removing with extra fields should not affect filter-only setting", + ), +] + + +@pytest.mark.admin +@pytest.mark.parametrize("test", pytest_params(REMOVEQUERYSETTINGS_SHAPE_PERSISTS_TESTS)) +def test_removeQuerySettings_shape_persists(collection, test): + """Test that mismatched shapes do not remove original settings.""" + ctx = CommandContext.from_collection(collection) + try: + for cmd in test.build_setup(ctx): + r = execute_admin_command(collection, cmd) + ctx.setup_results.append(r) + expected_hash = ctx.setup_results[0]["queryShapeHash"] + + execute_admin_command(collection, test.build_command(ctx)) + + admin = collection.database.client.admin + qs_result = admin.command( + {"aggregate": 1, "pipeline": [{"$querySettings": {}}], "cursor": {}} + ) + batch: list[dict[str, Any]] = qs_result.get("cursor", {}).get("firstBatch", []) + count = sum(1 for s in batch if s.get("queryShapeHash") == expected_hash) + assertSuccessPartial({"count": count}, {"count": 1}, msg=test.msg) + finally: + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass + + +# Property [Reject Removal Restores Query]: removing a reject: true setting +# allows the previously-rejected query to succeed again. +REMOVEQUERYSETTINGS_REJECT_REMOVAL_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( + "reject_removal_restores_query", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"rj1": 1}, + "$db": ctx.database, + }, + "settings": {"reject": True}, + } + ], + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"rj1": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"rj1": 1}, + "$db": ctx.database, + } + } + ], + msg="query should succeed after removing reject: true setting", + ), +] + + +@pytest.mark.admin +@pytest.mark.parametrize("test", pytest_params(REMOVEQUERYSETTINGS_REJECT_REMOVAL_TESTS)) +def test_removeQuerySettings_reject_removal(collection, test): + """Test that removing reject: true setting restores the query.""" + ctx = CommandContext.from_collection(collection) + try: + for cmd in test.build_setup(ctx): + r = execute_admin_command(collection, cmd) + ctx.setup_results.append(r) + + # Remove the reject setting + execute_admin_command(collection, test.build_command(ctx)) + + # Verify query succeeds after removal + restored = execute_command(collection, {"find": ctx.collection, "filter": {"rj1": 1}}) + assertSuccessPartial(restored, {"ok": 1.0}, msg=test.msg) + finally: + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_core.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_core.py new file mode 100644 index 000000000..0eff29d65 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_core.py @@ -0,0 +1,311 @@ +"""Tests for removeQuerySettings command core acceptance behavior. + +Validates that the removeQuerySettings command accepts valid query shapes +for find, distinct, and aggregate commands, various shape variations, +$db field variations, hash-based removal, and idempotent behavior. +""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.requires(cluster_admin=True), pytest.mark.no_parallel] + +# Property [Find Shape Acceptance]: removeQuerySettings accepts find shapes +# with various field combinations without error. +REMOVEQUERYSETTINGS_FIND_ACCEPTANCE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "accepts_find_filter_only", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept find with filter only", + ), + CommandTestCase( + "accepts_find_filter_sort", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b": 1}, + "sort": {"b": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept find with filter and sort", + ), + CommandTestCase( + "accepts_find_filter_projection", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"c": 1}, + "projection": {"c": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept find with filter and projection", + ), + CommandTestCase( + "accepts_find_filter_sort_projection", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"d": 1}, + "sort": {"d": 1}, + "projection": {"d": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept find with all shape fields", + ), + CommandTestCase( + "accepts_find_with_collation", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"e": "abc"}, + "collation": {"locale": "en", "strength": 2}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept find with collation", + ), + CommandTestCase( + "accepts_find_with_let", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"$expr": {"$eq": ["$f", "$$target"]}}, + "let": {"target": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept find with let", + ), + CommandTestCase( + "accepts_find_without_filter", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept find without filter", + ), +] + +# Property [Distinct Shape Acceptance]: removeQuerySettings accepts distinct +# shapes with various field combinations without error. +REMOVEQUERYSETTINGS_DISTINCT_ACCEPTANCE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "accepts_distinct_key_only", + command=lambda ctx: { + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "j", + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept distinct with key only", + ), + CommandTestCase( + "accepts_distinct_key_with_query", + command=lambda ctx: { + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "k", + "query": {"$and": [{"k": {"$gt": 0}}, {"k": {"$lt": 100}}]}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept distinct with query filter", + ), +] + +# Property [Aggregate Shape Acceptance]: removeQuerySettings accepts aggregate +# pipeline shapes with various stage combinations without error. +REMOVEQUERYSETTINGS_AGGREGATE_ACCEPTANCE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "accepts_aggregate_single_stage", + command=lambda ctx: { + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"l": 1}}], + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept aggregate with single stage", + ), + CommandTestCase( + "accepts_aggregate_multi_stage", + command=lambda ctx: { + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [ + {"$match": {"m": 1}}, + {"$group": {"_id": "$m", "count": {"$sum": 1}}}, + ], + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept aggregate with multiple stages", + ), +] + +# Property [Nonexistent $db Acceptance]: removeQuerySettings accepts +# non-existent database names in the $db field without error. +REMOVEQUERYSETTINGS_DB_ACCEPTANCE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "accepts_nonexistent_db", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"o": 1}, + "$db": f"{ctx.database}_nonexistent", + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept non-existent $db", + ), +] + +# Property [Silent No-Op]: removeQuerySettings succeeds silently when +# no matching settings exist. +REMOVEQUERYSETTINGS_NOOP_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "noop_nonexistent_query_shape", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"nonexistent_field": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should succeed when no matching settings exist", + ), + CommandTestCase( + "noop_nonexistent_hash", + command=lambda ctx: { + "removeQuerySettings": "00000000000000000000000000000000" + "00000000000000000000000000000000" + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should succeed with a non-existent hash", + ), + CommandTestCase( + "noop_lowercase_hash", + command=lambda ctx: { + "removeQuerySettings": "abcdef0123456789abcdef0123456789" + "abcdef0123456789abcdef0123456789" + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept lowercase hex hash", + ), +] + +# Property [IDHACK Query Acceptance]: unlike setQuerySettings which rejects +# IDHACK-eligible queries, removeQuerySettings accepts them. +REMOVEQUERYSETTINGS_IDHACK_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "accepts_idhack_query", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"_id": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept IDHACK-eligible queries", + ), +] + +# Property [Internal Database Acceptance]: unlike setQuerySettings which +# rejects internal databases, removeQuerySettings accepts them. +REMOVEQUERYSETTINGS_INTERNAL_DB_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "accepts_admin_db", + command=lambda ctx: { + "removeQuerySettings": { + "find": "system.users", + "filter": {}, + "$db": "admin", + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept admin database query shapes", + ), + CommandTestCase( + "accepts_local_db", + command=lambda ctx: { + "removeQuerySettings": { + "find": "oplog.rs", + "filter": {}, + "$db": "local", + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept local database query shapes", + ), +] + +# Property [Comment Field Acceptance]: removeQuerySettings accepts the +# comment top-level field without error. +REMOVEQUERYSETTINGS_COMMENT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "accepts_comment_field", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"cmt1": 1}, + "$db": ctx.database, + }, + "comment": "test comment", + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept comment field", + ), +] + +REMOVEQUERYSETTINGS_CORE_TESTS: list[CommandTestCase] = ( + REMOVEQUERYSETTINGS_FIND_ACCEPTANCE_TESTS + + REMOVEQUERYSETTINGS_DISTINCT_ACCEPTANCE_TESTS + + REMOVEQUERYSETTINGS_AGGREGATE_ACCEPTANCE_TESTS + + REMOVEQUERYSETTINGS_DB_ACCEPTANCE_TESTS + + REMOVEQUERYSETTINGS_NOOP_TESTS + + REMOVEQUERYSETTINGS_IDHACK_TESTS + + REMOVEQUERYSETTINGS_INTERNAL_DB_TESTS + + REMOVEQUERYSETTINGS_COMMENT_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(REMOVEQUERYSETTINGS_CORE_TESTS)) +def test_removeQuerySettings_core(collection, test): + """Test removeQuerySettings command core acceptance behavior.""" + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) + assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_error.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_error.py new file mode 100644 index 000000000..1a942cec6 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_error.py @@ -0,0 +1,212 @@ +"""Tests for removeQuerySettings command error cases. + +Validates that the removeQuerySettings command rejects invalid BSON types for +the primary argument, malformed query shapes, invalid hash strings, and +unrecognized top-level fields. +""" + +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + INVALID_LENGTH_ERROR, + INVALID_NAMESPACE_ERROR, + MISSING_FIELD_ERROR, + QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, + TYPE_MISMATCH_ERROR, + UNRECOGNIZED_COMMAND_FIELD_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.requires(cluster_admin=True), pytest.mark.no_parallel] + +# Property [Primary Argument Type Rejection]: the removeQuerySettings field +# must be a document or string. All other BSON types are rejected. +REMOVEQUERYSETTINGS_PRIMARY_ARG_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + f"primary_arg_{tid}", + command=lambda ctx, v=value: {"removeQuerySettings": v}, + error_code=TYPE_MISMATCH_ERROR, + msg=f"removeQuerySettings should reject {tid} as the primary argument", + ) + for tid, value in [ + ("null", None), + ("int32", 42), + ("int64", Int64(42)), + ("double", 3.14), + ("decimal128", Decimal128("1")), + ("bool_true", True), + ("bool_false", False), + ("array", [1, 2, 3]), + ("objectid", ObjectId()), + ("datetime", datetime(2024, 1, 1, tzinfo=timezone.utc)), + ("timestamp", Timestamp(0, 0)), + ("binary", Binary(b"\x00")), + ("regex", Regex(".*")), + ("code", Code("function(){}")), + ("minkey", MinKey()), + ("maxkey", MaxKey()), + ] +] + +# Property [Query Shape Validation]: rejects malformed query shape documents +# including empty documents, missing/empty/null $db, and unknown command types. +REMOVEQUERYSETTINGS_QUERY_SHAPE_VALIDATION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "query_shape_empty_document", + command=lambda ctx: {"removeQuerySettings": {}}, + error_code=QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, + msg="removeQuerySettings should reject empty query shape document", + ), + CommandTestCase( + "query_shape_missing_db", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + } + }, + error_code=MISSING_FIELD_ERROR, + msg="removeQuerySettings should reject query shape missing $db field", + ), + CommandTestCase( + "query_shape_empty_db", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": "", + } + }, + error_code=INVALID_NAMESPACE_ERROR, + msg="removeQuerySettings should reject query shape with empty $db", + ), + CommandTestCase( + "query_shape_null_db", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": None, + } + }, + error_code=MISSING_FIELD_ERROR, + msg="removeQuerySettings should reject query shape with null $db", + ), + CommandTestCase( + "query_shape_unknown_command", + command=lambda ctx: { + "removeQuerySettings": { + "unknownCommand": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + } + }, + error_code=QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, + msg="removeQuerySettings should reject unknown command type in query shape", + ), + CommandTestCase( + "query_shape_no_command_type", + command=lambda ctx: { + "removeQuerySettings": { + "filter": {"x": 1}, + "$db": ctx.database, + } + }, + error_code=QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, + msg="removeQuerySettings should reject query shape without a command type", + ), +] + +# Property [Hash String Validation]: rejects invalid hash string formats +# including empty, too short, too long, and non-hexadecimal strings. +REMOVEQUERYSETTINGS_HASH_VALIDATION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "empty_hash_string", + command=lambda ctx: {"removeQuerySettings": ""}, + error_code=INVALID_LENGTH_ERROR, + msg="removeQuerySettings should reject empty hash string", + ), + CommandTestCase( + "short_hash_string", + command=lambda ctx: {"removeQuerySettings": "ABCD"}, + error_code=INVALID_LENGTH_ERROR, + msg="removeQuerySettings should reject short hash string", + ), + CommandTestCase( + "long_hash_string", + command=lambda ctx: {"removeQuerySettings": "AA" * 33}, + error_code=INVALID_LENGTH_ERROR, + msg="removeQuerySettings should reject hash string longer than 64 chars", + ), + CommandTestCase( + "non_hex_hash_string", + command=lambda ctx: { + "removeQuerySettings": "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG" + "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG" + }, + error_code=BAD_VALUE_ERROR, + msg="removeQuerySettings should reject non-hex hash string", + ), +] + +# Property [Unrecognized Fields]: rejects unknown top-level command fields +# and fields valid for setQuerySettings but not removeQuerySettings. +REMOVEQUERYSETTINGS_UNRECOGNIZED_FIELD_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "unrecognized_top_level_field", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, + "unknownField": 1, + }, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="removeQuerySettings should reject unrecognized top-level field", + ), + CommandTestCase( + "settings_field_rejected", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, + "settings": {"reject": True}, + }, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="removeQuerySettings should reject settings field", + ), +] + +REMOVEQUERYSETTINGS_ERROR_TESTS: list[CommandTestCase] = ( + REMOVEQUERYSETTINGS_PRIMARY_ARG_TYPE_TESTS + + REMOVEQUERYSETTINGS_QUERY_SHAPE_VALIDATION_TESTS + + REMOVEQUERYSETTINGS_HASH_VALIDATION_TESTS + + REMOVEQUERYSETTINGS_UNRECOGNIZED_FIELD_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(REMOVEQUERYSETTINGS_ERROR_TESTS)) +def test_removeQuerySettings_error(collection, test): + """Test removeQuerySettings error cases.""" + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + error_code=test.error_code, + msg=test.msg, + )