From ba2a6280749d24e51ff460fd34c54613dbab4b5c Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 12 Jun 2026 13:41:32 -0700 Subject: [PATCH 01/27] initial generated tests Signed-off-by: Alina (Xi) Li --- .../commands/setQuerySettings/__init__.py | 0 .../test_setQuerySettings_behavior.py | 376 +++++++++++ .../test_setQuerySettings_query_shapes.py | 598 ++++++++++++++++++ .../test_setQuerySettings_settings.py | 445 +++++++++++++ .../test_setQuerySettings_type_errors.py | 371 +++++++++++ ...test_setQuerySettings_validation_errors.py | 407 ++++++++++++ documentdb_tests/framework/error_codes.py | 8 + 7 files changed, 2205 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py create mode 100644 documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py create mode 100644 documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/__init__.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py new file mode 100644 index 000000000..c9de40940 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py @@ -0,0 +1,376 @@ +"""Tests for setQuerySettings command behavioral verification. + +Validates that query settings are retrievable via $querySettings aggregation +stage, removable via removeQuerySettings, and that the response structure +includes expected fields like queryShapeHash and representativeQuery. +""" + +from __future__ import annotations + +from typing import Any + +import pytest +from pymongo.collection import Collection + +from documentdb_tests.framework.assertions import assertResult, assertSuccessPartial +from documentdb_tests.framework.error_codes import QUERYSETTINGS_QUERY_REJECTED_ERROR +from documentdb_tests.framework.executor import execute_admin_command, execute_command + + +def _cleanup(collection: Collection, queries: list[dict]) -> None: + """Remove all query settings created during the test.""" + admin = collection.database.client.admin + for q in queries: + try: + admin.command({"removeQuerySettings": q}) + except Exception: + pass + + +def _get_settings(collection: Collection) -> list[dict[str, Any]]: + """Retrieve all current query settings via $querySettings stage.""" + admin = collection.database.client.admin + result = admin.command({"aggregate": 1, "pipeline": [{"$querySettings": {}}], "cursor": {}}) + batch: list[dict[str, Any]] = result.get("cursor", {}).get("firstBatch", []) + return batch + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_response_contains_hash(collection: Collection): + """Test setQuerySettings response contains queryShapeHash field.""" + query = { + "find": collection.name, + "filter": {"b1": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0, "queryShapeHash": result.get("queryShapeHash")}, + msg="response should contain queryShapeHash", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_response_contains_representative_query(collection: Collection): + """Test setQuerySettings response contains representativeQuery field.""" + query = { + "find": collection.name, + "filter": {"b2": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0, "representativeQuery": result.get("representativeQuery")}, + msg="response should contain representativeQuery", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_response_settings_echo(collection: Collection): + """Test setQuerySettings response echoes the settings that were applied.""" + query = { + "find": collection.name, + "filter": {"b3": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + { + "ok": 1.0, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + msg="response should echo applied settings", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_querySettings_stage_retrieval(collection: Collection): + """Test query settings are visible via $querySettings aggregation stage.""" + query = { + "find": collection.name, + "filter": {"b4": 1}, + "$db": collection.database.name, + } + try: + # Setup: create a query setting (no assertion — setup only) + setup_result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + expected_hash = setup_result.get("queryShapeHash") + + settings = _get_settings(collection) + matching = [s for s in settings if s.get("queryShapeHash") == expected_hash] + assertSuccessPartial( + matching[0] if matching else {}, + {"queryShapeHash": expected_hash}, + msg="$querySettings should return the created setting", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_removeQuerySettings_by_query(collection: Collection): + """Test removeQuerySettings removes settings by representative query.""" + query = { + "find": collection.name, + "filter": {"b5": 1}, + "$db": collection.database.name, + } + try: + # Setup: create a query setting (no assertion — setup only) + execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + + result = execute_admin_command( + collection, + {"removeQuerySettings": query}, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="removeQuerySettings by query should succeed") + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_removeQuerySettings_by_hash(collection: Collection): + """Test removeQuerySettings removes settings by query shape hash.""" + query = { + "find": collection.name, + "filter": {"b6": 1}, + "$db": collection.database.name, + } + try: + # Setup: create a query setting and capture hash (no assertion — setup only) + setup_result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + + query_hash = setup_result.get("queryShapeHash") + result = execute_admin_command( + collection, + {"removeQuerySettings": query_hash}, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="removeQuerySettings by hash should succeed") + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_reject_true_blocks_query(collection: Collection): + """Test that reject: true causes the matching query to be rejected.""" + query = { + "find": collection.name, + "filter": {"b8": 1}, + "$db": collection.database.name, + } + try: + # Setup: create a reject setting (no assertion — setup only) + execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": {"reject": True}, + }, + ) + + # Execute the matching find query on the collection database + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"b8": 1}, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, + msg="query matching reject: true setting should be rejected", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_querySettings_stage_shows_settings(collection: Collection): + """Test $querySettings stage includes indexHints in the returned settings.""" + query = { + "find": collection.name, + "filter": {"b9": 1}, + "$db": collection.database.name, + } + try: + # Setup: create a query setting (no assertion — setup only) + setup_result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + expected_hash = setup_result.get("queryShapeHash") + + settings = _get_settings(collection) + matching = [s for s in settings if s.get("queryShapeHash") == expected_hash] + entry = matching[0] if matching else {} + assertSuccessPartial( + entry, + { + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + msg="$querySettings should include indexHints in settings", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_querySettings_stage_shows_representative_query(collection: Collection): + """Test $querySettings stage includes representativeQuery in the output.""" + query = { + "find": collection.name, + "filter": {"b10": 1}, + "$db": collection.database.name, + } + try: + # Setup: create a query setting (no assertion — setup only) + setup_result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + expected_hash = setup_result.get("queryShapeHash") + + settings = _get_settings(collection) + matching = [s for s in settings if s.get("queryShapeHash") == expected_hash] + entry = matching[0] if matching else {} + assertSuccessPartial( + entry, + {"representativeQuery": entry.get("representativeQuery")}, + msg="$querySettings should include representativeQuery", + ) + finally: + _cleanup(collection, [query]) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py new file mode 100644 index 000000000..38b81787c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py @@ -0,0 +1,598 @@ +"""Tests for setQuerySettings command query shape acceptance. + +Validates that the setQuerySettings command accepts valid query shapes for +find, distinct, and aggregate commands, including various shape variations, +field combinations, and $db field variations. +""" + +from __future__ import annotations + +import pytest +from pymongo.collection import Collection + +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command + + +def _cleanup(collection: Collection, queries: list[dict]) -> None: + """Remove all query settings created during the test.""" + admin = collection.database.client.admin + for q in queries: + try: + admin.command({"removeQuerySettings": q}) + except Exception: + pass + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_find_shape(collection: Collection): + """Test setQuerySettings accepts a valid find query shape.""" + query = { + "find": collection.name, + "filter": {"x": 1}, + "sort": {"x": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept valid find shape", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_distinct_shape(collection: Collection): + """Test setQuerySettings accepts a valid distinct query shape.""" + query = { + "distinct": collection.name, + "key": "x", + "query": {"x": {"$gt": 0}}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept valid distinct shape", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_aggregate_shape(collection: Collection): + """Test setQuerySettings accepts a valid aggregate query shape.""" + query = { + "aggregate": collection.name, + "pipeline": [{"$match": {"x": 1}}], + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept valid aggregate shape", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_find_filter_only(collection: Collection): + """Test setQuerySettings accepts find shape with only filter, no sort or projection.""" + query = { + "find": collection.name, + "filter": {"a": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept find with filter only", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_find_filter_sort(collection: Collection): + """Test setQuerySettings accepts find shape with filter and sort.""" + query = { + "find": collection.name, + "filter": {"b": 1}, + "sort": {"b": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept find with filter+sort", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_find_filter_projection(collection: Collection): + """Test setQuerySettings accepts find shape with filter and projection.""" + query = { + "find": collection.name, + "filter": {"c": 1}, + "projection": {"c": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept find with filter+projection", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_find_filter_sort_projection(collection: Collection): + """Test setQuerySettings accepts find shape with filter, sort, and projection.""" + query = { + "find": collection.name, + "filter": {"d": 1}, + "sort": {"d": 1}, + "projection": {"d": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept find with all fields", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_find_with_collation(collection: Collection): + """Test setQuerySettings accepts find shape with collation.""" + query = { + "find": collection.name, + "filter": {"e": "abc"}, + "collation": {"locale": "en", "strength": 2}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept find with collation", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_find_with_let(collection: Collection): + """Test setQuerySettings accepts find shape with let variables.""" + query = { + "find": collection.name, + "filter": {"$expr": {"$eq": ["$f", "$$target"]}}, + "let": {"target": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept find with let", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_find_with_limit(collection: Collection): + """Test setQuerySettings accepts find shape containing limit.""" + query = { + "find": collection.name, + "filter": {"g": 1}, + "limit": 10, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept find with limit", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_distinct_key_only(collection: Collection): + """Test setQuerySettings accepts distinct shape with key only, no query filter.""" + query = { + "distinct": collection.name, + "key": "j", + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept distinct key only", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_distinct_complex_query(collection: Collection): + """Test setQuerySettings accepts distinct shape with complex query filter.""" + query = { + "distinct": collection.name, + "key": "k", + "query": {"$and": [{"k": {"$gt": 0}}, {"k": {"$lt": 100}}]}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept distinct complex query", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_aggregate_match_only(collection: Collection): + """Test setQuerySettings accepts aggregate shape with single $match stage.""" + query = { + "aggregate": collection.name, + "pipeline": [{"$match": {"l": 1}}], + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept aggregate $match only", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_aggregate_match_group(collection: Collection): + """Test setQuerySettings accepts aggregate shape with $match and $group pipeline.""" + query = { + "aggregate": collection.name, + "pipeline": [{"$match": {"m": 1}}, {"$group": {"_id": "$m", "count": {"$sum": 1}}}], + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept aggregate $match+$group", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_aggregate_match_sort_limit(collection: Collection): + """Test setQuerySettings accepts aggregate shape with $match, $sort, and $limit.""" + query = { + "aggregate": collection.name, + "pipeline": [{"$match": {"n": 1}}, {"$sort": {"n": 1}}, {"$limit": 5}], + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept aggregate $match+$sort+$limit", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_db_nonexistent(collection: Collection): + """Test setQuerySettings accepts $db pointing to a non-existent database.""" + query = { + "find": collection.name, + "filter": {"o": 1}, + "$db": "nonexistent_db_for_query_settings_test", + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": { + "db": "nonexistent_db_for_query_settings_test", + "coll": collection.name, + }, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept non-existent $db", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_db_special_characters(collection: Collection): + """Test setQuerySettings accepts $db with special characters like hyphens.""" + query = { + "find": collection.name, + "filter": {"p": 1}, + "$db": "test-special-db", + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": "test-special-db", "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept $db with special chars", + ) + finally: + _cleanup(collection, [query]) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py new file mode 100644 index 000000000..e6f98ac5c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py @@ -0,0 +1,445 @@ +"""Tests for setQuerySettings command settings configurations. + +Validates that the setQuerySettings command accepts valid settings +combinations including indexHints, reject, queryFramework, and comment +fields, as well as allowedIndexes variations and update behavior. +""" + +from __future__ import annotations + +import pytest +from pymongo.collection import Collection + +from documentdb_tests.framework.assertions import assertResult, assertSuccessPartial +from documentdb_tests.framework.error_codes import ( + QUERYSETTINGS_IDHACK_QUERY_ERROR, + QUERYSETTINGS_REJECT_ONLY_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command + + +def _cleanup(collection: Collection, queries: list[dict]) -> None: + """Remove all query settings created during the test.""" + admin = collection.database.client.admin + for q in queries: + try: + admin.command({"removeQuerySettings": q}) + except Exception: + pass + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_indexHints_single_index(collection: Collection): + """Test setQuerySettings accepts indexHints with a single named index.""" + query = { + "find": collection.name, + "filter": {"a1": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="should accept indexHints with single index") + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_indexHints_multiple_indexes(collection: Collection): + """Test setQuerySettings accepts indexHints with multiple allowedIndexes entries.""" + query = { + "find": collection.name, + "filter": {"a2": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_", {"a2": 1}], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept multiple indexes", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_indexHints_key_pattern(collection: Collection): + """Test setQuerySettings accepts indexHints with index key pattern instead of name.""" + query = { + "find": collection.name, + "filter": {"a3": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": [{"a3": 1}], + } + ], + }, + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="should accept indexHints with key pattern") + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_indexHints_empty_allowed_rejected(collection: Collection): + """Test setQuerySettings rejects indexHints with empty allowedIndexes as empty settings.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"a4": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": [], + } + ], + }, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_REJECT_ONLY_ERROR, + msg="should reject indexHints with empty allowedIndexes", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_reject_true(collection: Collection): + """Test setQuerySettings accepts settings with reject: true.""" + query = { + "find": collection.name, + "filter": {"a5": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": {"reject": True}, + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="should accept settings with reject: true") + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_reject_with_indexHints(collection: Collection): + """Test setQuerySettings accepts settings with both reject and indexHints.""" + query = { + "find": collection.name, + "filter": {"a6": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + "reject": True, + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="should accept reject with indexHints", + ) + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_queryFramework_classic(collection: Collection): + """Test setQuerySettings accepts queryFramework: classic.""" + query = { + "find": collection.name, + "filter": {"a7": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": "classic", + }, + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="should accept queryFramework: classic") + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_queryFramework_sbe(collection: Collection): + """Test setQuerySettings accepts queryFramework: sbe.""" + query = { + "find": collection.name, + "filter": {"a8": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": "sbe", + }, + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="should accept queryFramework: sbe") + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_with_comment_string(collection: Collection): + """Test setQuerySettings accepts a comment field with string value.""" + query = { + "find": collection.name, + "filter": {"a9": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + "comment": "test comment for setQuerySettings", + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="should accept command with comment string") + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_update_existing_settings(collection: Collection): + """Test setQuerySettings can update settings for an existing query shape.""" + query = { + "find": collection.name, + "filter": {"a10": 1}, + "$db": collection.database.name, + } + try: + # Setup: create initial settings (no assertion — setup only) + execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_", {"a10": 1}], + } + ], + }, + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="update setQuerySettings should succeed") + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_update_via_hash(collection: Collection): + """Test setQuerySettings can update settings using the query shape hash.""" + query = { + "find": collection.name, + "filter": {"a11": 1}, + "$db": collection.database.name, + } + try: + # Setup: create initial settings and capture hash (no assertion — setup only) + setup_result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + + query_hash = setup_result.get("queryShapeHash") + result = execute_admin_command( + collection, + { + "setQuerySettings": query_hash, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_", {"a11": 1}], + } + ], + }, + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="update via hash should succeed") + finally: + _cleanup(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_idhack_query_rejected(collection: Collection): + """Test setQuerySettings rejects queries eligible for IDHACK optimization.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"_id": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_IDHACK_QUERY_ERROR, + msg="setQuerySettings should reject IDHACK-eligible queries", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_all_settings_combined(collection: Collection): + """Test setQuerySettings accepts all settings fields combined.""" + query = { + "find": collection.name, + "filter": {"a12": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": "classic", + "reject": True, + }, + }, + ) + assertSuccessPartial(result, {"ok": 1.0}, msg="should accept all settings combined") + finally: + _cleanup(collection, [query]) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py new file mode 100644 index 000000000..45c9936c7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py @@ -0,0 +1,371 @@ +"""Tests for setQuerySettings command BSON type rejection. + +Validates that the setQuerySettings command rejects invalid BSON types for +the primary argument field, the queryFramework sub-field, the reject sub-field, +and the indexHints namespace and allowedIndexes sub-fields. +""" + +from __future__ import annotations + +from datetime import datetime, timezone +from typing import Any + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp +from pymongo.collection import Collection + +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + FAILED_TO_PARSE_ERROR, + MISSING_FIELD_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command + +# Property [Primary Argument Type Rejection]: the setQuerySettings field must +# be a document (query shape) or string (hash). All other BSON types are +# rejected with TYPE_MISMATCH_ERROR. +_PRIMARY_ARG_INVALID_TYPES: list[tuple[str, Any]] = [ + ("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 [queryFramework Type Rejection]: the queryFramework field must be a +# string. Non-string BSON types are rejected with TYPE_MISMATCH_ERROR. +_QUERY_FRAMEWORK_INVALID_TYPES: list[tuple[str, Any]] = [ + ("int32", 42), + ("int64", Int64(42)), + ("double", 3.14), + ("decimal128", Decimal128("1")), + ("bool_true", True), + ("bool_false", False), + ("array", [1]), + ("object", {"k": "v"}), + ("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 [reject Type Rejection]: the reject field must be a boolean. +# Non-boolean BSON types are rejected with TYPE_MISMATCH_ERROR. +_REJECT_INVALID_TYPES: list[tuple[str, Any]] = [ + ("null", None), + ("int32", 42), + ("int64", Int64(42)), + ("double", 3.14), + ("decimal128", Decimal128("1")), + ("string", "true"), + ("array", [True]), + ("object", {"k": "v"}), + ("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 [indexHints.ns.db Type Rejection]: the ns.db field must be a string. +_NS_DB_INVALID_TYPES: list[tuple[str, Any]] = [ + ("int32", 42), + ("bool", True), + ("array", ["test"]), + ("object", {"k": "v"}), +] + +# Property [indexHints.ns.coll Type Rejection]: the ns.coll field must be a string. +_NS_COLL_INVALID_TYPES: list[tuple[str, Any]] = [ + ("int32", 42), + ("bool", True), +] + +# Property [indexHints.allowedIndexes Type Rejection]: allowedIndexes must be an array. +_ALLOWED_INDEXES_INVALID_TYPES: list[tuple[str, Any]] = [ + ("string", "_id_"), + ("int32", 42), +] + + +@pytest.mark.admin +@pytest.mark.replica_set +@pytest.mark.parametrize( + "tid, value", + _PRIMARY_ARG_INVALID_TYPES, + ids=[t[0] for t in _PRIMARY_ARG_INVALID_TYPES], +) +def test_setQuerySettings_primary_arg_type_rejection(collection: Collection, tid: str, value: Any): + """Test setQuerySettings rejects invalid BSON types for the primary argument.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": value, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=TYPE_MISMATCH_ERROR, + msg=f"setQuerySettings should reject {tid} as the primary argument", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +@pytest.mark.parametrize( + "tid, value", + _QUERY_FRAMEWORK_INVALID_TYPES, + ids=[t[0] for t in _QUERY_FRAMEWORK_INVALID_TYPES], +) +def test_setQuerySettings_query_framework_type_rejection( + collection: Collection, tid: str, value: Any +): + """Test setQuerySettings rejects invalid BSON types for queryFramework.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": value, + }, + }, + ) + assertResult( + result, + error_code=TYPE_MISMATCH_ERROR, + msg=f"setQuerySettings should reject {tid} as queryFramework", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +@pytest.mark.parametrize( + "tid, value", + _REJECT_INVALID_TYPES, + ids=[t[0] for t in _REJECT_INVALID_TYPES], +) +def test_setQuerySettings_reject_type_rejection(collection: Collection, tid: str, value: Any): + """Test setQuerySettings rejects invalid BSON types for reject field.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + "reject": value, + }, + }, + ) + assertResult( + result, + error_code=TYPE_MISMATCH_ERROR, + msg=f"setQuerySettings should reject {tid} as reject field", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +@pytest.mark.parametrize( + "tid, value", + _NS_DB_INVALID_TYPES, + ids=[t[0] for t in _NS_DB_INVALID_TYPES], +) +def test_setQuerySettings_ns_db_type_rejection(collection: Collection, tid: str, value: Any): + """Test setQuerySettings rejects invalid BSON types for indexHints.ns.db.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": value, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=TYPE_MISMATCH_ERROR, + msg=f"setQuerySettings should reject {tid} as indexHints.ns.db", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +@pytest.mark.parametrize( + "tid, value", + _NS_COLL_INVALID_TYPES, + ids=[t[0] for t in _NS_COLL_INVALID_TYPES], +) +def test_setQuerySettings_ns_coll_type_rejection(collection: Collection, tid: str, value: Any): + """Test setQuerySettings rejects invalid BSON types for indexHints.ns.coll.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": value}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=TYPE_MISMATCH_ERROR, + msg=f"setQuerySettings should reject {tid} as indexHints.ns.coll", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +@pytest.mark.parametrize( + "tid, value", + _ALLOWED_INDEXES_INVALID_TYPES, + ids=[t[0] for t in _ALLOWED_INDEXES_INVALID_TYPES], +) +def test_setQuerySettings_allowed_indexes_type_rejection( + collection: Collection, tid: str, value: Any +): + """Test setQuerySettings rejects invalid BSON types for indexHints.allowedIndexes.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": value, + } + ], + }, + }, + ) + assertResult( + result, + error_code=TYPE_MISMATCH_ERROR, + msg=f"setQuerySettings should reject {tid} as indexHints.allowedIndexes", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_allowed_indexes_null_missing(collection: Collection): + """Test setQuerySettings rejects null allowedIndexes as missing required field.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": None, + } + ], + }, + }, + ) + assertResult( + result, + error_code=MISSING_FIELD_ERROR, + msg="setQuerySettings should reject null allowedIndexes as missing field", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_allowed_indexes_non_string_element(collection: Collection): + """Test setQuerySettings rejects non-string elements in allowedIndexes array.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": [42], + } + ], + }, + }, + ) + assertResult( + result, + error_code=FAILED_TO_PARSE_ERROR, + msg="setQuerySettings should reject non-string elements in allowedIndexes", + ) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py new file mode 100644 index 000000000..39a01dc26 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py @@ -0,0 +1,407 @@ +"""Tests for setQuerySettings command structural and validation errors. + +Validates that the setQuerySettings command rejects malformed query shapes, +invalid hash strings, missing or empty settings, unrecognized fields, invalid +queryFramework values, and system collection restrictions. +""" + +from __future__ import annotations + +import pytest +from pymongo.collection import Collection + +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + INVALID_NAMESPACE_ERROR, + MISSING_FIELD_ERROR, + QUERYSETTINGS_EMPTY_SETTINGS_ERROR, + QUERYSETTINGS_INTERNAL_DB_ERROR, + QUERYSETTINGS_NS_COLL_MISSING_ERROR, + QUERYSETTINGS_NS_DB_MISSING_ERROR, + QUERYSETTINGS_REJECT_ONLY_ERROR, + QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, + INVALID_LENGTH_ERROR, + UNRECOGNIZED_COMMAND_FIELD_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_query_shape_missing_db(collection: Collection): + """Test setQuerySettings rejects a query shape document missing $db field.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=MISSING_FIELD_ERROR, + msg="setQuerySettings should reject query shape missing $db field", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_query_shape_empty_db(collection: Collection): + """Test setQuerySettings rejects a query shape with empty string $db.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": "", + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=INVALID_NAMESPACE_ERROR, + msg="setQuerySettings should reject query shape with empty $db", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_query_shape_unknown_command(collection: Collection): + """Test setQuerySettings rejects a query shape with an unknown command type.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "unknownCommand": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, + msg="setQuerySettings should reject unknown command type in query shape", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_empty_hash_string(collection: Collection): + """Test setQuerySettings rejects an empty hash string.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": "", + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=INVALID_LENGTH_ERROR, + msg="setQuerySettings should reject empty hash string", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_indexHints_missing_ns(collection: Collection): + """Test setQuerySettings rejects indexHints entry missing ns field.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=MISSING_FIELD_ERROR, + msg="setQuerySettings should reject indexHints missing ns field", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_indexHints_ns_missing_db(collection: Collection): + """Test setQuerySettings rejects indexHints.ns missing db field.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_NS_DB_MISSING_ERROR, + msg="setQuerySettings should reject indexHints.ns missing db field", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_indexHints_ns_missing_coll(collection: Collection): + """Test setQuerySettings rejects indexHints.ns missing coll field.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_NS_COLL_MISSING_ERROR, + msg="setQuerySettings should reject indexHints.ns missing coll field", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_invalid_query_framework_value(collection: Collection): + """Test setQuerySettings rejects an invalid queryFramework string value.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": "invalidFramework", + }, + }, + ) + assertResult( + result, + error_code=BAD_VALUE_ERROR, + msg="setQuerySettings should reject invalid queryFramework string", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_reject_false_only(collection: Collection): + """Test setQuerySettings rejects settings with only reject: false and no other settings.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": {"reject": False}, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_REJECT_ONLY_ERROR, + msg="setQuerySettings should reject settings with only reject: false", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_missing_settings(collection: Collection): + """Test setQuerySettings rejects command missing the settings field entirely.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + }, + ) + assertResult( + result, + error_code=MISSING_FIELD_ERROR, + msg="setQuerySettings should reject missing settings field", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_empty_settings(collection: Collection): + """Test setQuerySettings rejects empty settings document.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": {}, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_EMPTY_SETTINGS_ERROR, + msg="setQuerySettings should reject empty settings document", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_unrecognized_top_level_field(collection: Collection): + """Test setQuerySettings rejects unrecognized top-level fields.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + "unknownField": 1, + }, + ) + assertResult( + result, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="setQuerySettings should reject unrecognized top-level field", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_system_collection(collection: Collection): + """Test setQuerySettings rejects query shapes targeting internal databases.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": "system.users", + "filter": {}, + "$db": "admin", + }, + "settings": { + "indexHints": [ + { + "ns": {"db": "admin", "coll": "system.users"}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_INTERNAL_DB_ERROR, + msg="setQuerySettings should reject query shapes on internal databases", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_local_database(collection: Collection): + """Test setQuerySettings rejects query shapes targeting local database.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": "oplog.rs", + "filter": {}, + "$db": "local", + }, + "settings": { + "indexHints": [ + { + "ns": {"db": "local", "coll": "oplog.rs"}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_INTERNAL_DB_ERROR, + msg="setQuerySettings should reject query shapes on local database", + ) diff --git a/documentdb_tests/framework/error_codes.py b/documentdb_tests/framework/error_codes.py index fd892adbc..7c137d582 100644 --- a/documentdb_tests/framework/error_codes.py +++ b/documentdb_tests/framework/error_codes.py @@ -53,6 +53,7 @@ API_VERSION_ERROR = 322 API_STRICT_ERROR = 323 COLLECTION_UUID_MISMATCH_ERROR = 361 +QUERYSETTINGS_QUERY_REJECTED_ERROR = 411 EXPRESSION_NOT_OBJECT_ERROR = 10065 BSON_OBJECT_TOO_LARGE_ERROR = 10334 DUPLICATE_KEY_ERROR = 11000 @@ -500,11 +501,18 @@ N_ACCUMULATOR_INVALID_N_ERROR = 7548606 GEO_NEAR_MIN_DISTANCE_NOT_CONSTANT_ERROR = 7555701 GEO_NEAR_MAX_DISTANCE_NOT_CONSTANT_ERROR = 7555702 +QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR = 7746402 +QUERYSETTINGS_REJECT_ONLY_ERROR = 7746604 +QUERYSETTINGS_IDHACK_QUERY_ERROR = 7746606 QUERYSETTINGS_NON_DOCUMENT_ARG_ERROR = 7746800 PIPELINE_LENGTH_LIMIT_ERROR = 7749501 PERCENTILE_INVALID_P_FIELD_ERROR = 7750301 PERCENTILE_INVALID_P_VALUE_ERROR = 7750303 ENCRYPTED_FIELD_TRIM_FACTOR_OUT_OF_RANGE_ERROR = 8574000 +QUERYSETTINGS_INTERNAL_DB_ERROR = 8584900 +QUERYSETTINGS_NS_DB_MISSING_ERROR = 8727500 +QUERYSETTINGS_NS_COLL_MISSING_ERROR = 8727501 +QUERYSETTINGS_EMPTY_SETTINGS_ERROR = 8727502 COUNT_FIELD_ID_RESERVED_ERROR = 9039800 CONVERT_BYTE_ORDER_TYPE_ERROR = 9130001 CONVERT_BYTE_ORDER_VALUE_ERROR = 9130002 From d1774d1b54ccfee6cc1e50e8c10dbb641a80c723 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 12 Jun 2026 14:18:16 -0700 Subject: [PATCH 02/27] use style guide Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_behavior.py | 48 +++------ .../test_setQuerySettings_query_shapes.py | 49 ++++----- .../test_setQuerySettings_settings.py | 102 ++++-------------- ...test_setQuerySettings_validation_errors.py | 67 ++++++++++++ .../setQuerySettings/utils/__init__.py | 0 .../utils/setQuerySettings_common.py | 25 +++++ 6 files changed, 151 insertions(+), 140 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/utils/setQuerySettings_common.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py index c9de40940..afa0db906 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py @@ -7,8 +7,6 @@ from __future__ import annotations -from typing import Any - import pytest from pymongo.collection import Collection @@ -16,25 +14,10 @@ from documentdb_tests.framework.error_codes import QUERYSETTINGS_QUERY_REJECTED_ERROR from documentdb_tests.framework.executor import execute_admin_command, execute_command - -def _cleanup(collection: Collection, queries: list[dict]) -> None: - """Remove all query settings created during the test.""" - admin = collection.database.client.admin - for q in queries: - try: - admin.command({"removeQuerySettings": q}) - except Exception: - pass - - -def _get_settings(collection: Collection) -> list[dict[str, Any]]: - """Retrieve all current query settings via $querySettings stage.""" - admin = collection.database.client.admin - result = admin.command({"aggregate": 1, "pipeline": [{"$querySettings": {}}], "cursor": {}}) - batch: list[dict[str, Any]] = result.get("cursor", {}).get("firstBatch", []) - return batch +from .utils.setQuerySettings_common import cleanup_query_settings, get_query_settings +# Property [Response Structure]: setQuerySettings response includes hash, query, and settings. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_response_contains_hash(collection: Collection): @@ -65,7 +48,7 @@ def test_setQuerySettings_response_contains_hash(collection: Collection): msg="response should contain queryShapeHash", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -98,7 +81,7 @@ def test_setQuerySettings_response_contains_representative_query(collection: Col msg="response should contain representativeQuery", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -141,9 +124,10 @@ def test_setQuerySettings_response_settings_echo(collection: Collection): msg="response should echo applied settings", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) +# Property [$querySettings Retrieval]: settings are visible via $querySettings aggregation stage. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_querySettings_stage_retrieval(collection: Collection): @@ -171,7 +155,7 @@ def test_setQuerySettings_querySettings_stage_retrieval(collection: Collection): ) expected_hash = setup_result.get("queryShapeHash") - settings = _get_settings(collection) + settings = get_query_settings(collection) matching = [s for s in settings if s.get("queryShapeHash") == expected_hash] assertSuccessPartial( matching[0] if matching else {}, @@ -179,9 +163,10 @@ def test_setQuerySettings_querySettings_stage_retrieval(collection: Collection): msg="$querySettings should return the created setting", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) +# Property [removeQuerySettings]: settings can be removed by query or hash. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_removeQuerySettings_by_query(collection: Collection): @@ -214,7 +199,7 @@ def test_setQuerySettings_removeQuerySettings_by_query(collection: Collection): ) assertSuccessPartial(result, {"ok": 1.0}, msg="removeQuerySettings by query should succeed") finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -250,9 +235,10 @@ def test_setQuerySettings_removeQuerySettings_by_hash(collection: Collection): ) assertSuccessPartial(result, {"ok": 1.0}, msg="removeQuerySettings by hash should succeed") finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) +# Property [Reject Blocks Query]: a rejected query returns an error when executed. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_reject_true_blocks_query(collection: Collection): @@ -286,7 +272,7 @@ def test_setQuerySettings_reject_true_blocks_query(collection: Collection): msg="query matching reject: true setting should be rejected", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -316,7 +302,7 @@ def test_setQuerySettings_querySettings_stage_shows_settings(collection: Collect ) expected_hash = setup_result.get("queryShapeHash") - settings = _get_settings(collection) + settings = get_query_settings(collection) matching = [s for s in settings if s.get("queryShapeHash") == expected_hash] entry = matching[0] if matching else {} assertSuccessPartial( @@ -334,7 +320,7 @@ def test_setQuerySettings_querySettings_stage_shows_settings(collection: Collect msg="$querySettings should include indexHints in settings", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -364,7 +350,7 @@ def test_setQuerySettings_querySettings_stage_shows_representative_query(collect ) expected_hash = setup_result.get("queryShapeHash") - settings = _get_settings(collection) + settings = get_query_settings(collection) matching = [s for s in settings if s.get("queryShapeHash") == expected_hash] entry = matching[0] if matching else {} assertSuccessPartial( @@ -373,4 +359,4 @@ def test_setQuerySettings_querySettings_stage_shows_representative_query(collect msg="$querySettings should include representativeQuery", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py index 38b81787c..5d938749a 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py @@ -13,17 +13,10 @@ from documentdb_tests.framework.assertions import assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command - -def _cleanup(collection: Collection, queries: list[dict]) -> None: - """Remove all query settings created during the test.""" - admin = collection.database.client.admin - for q in queries: - try: - admin.command({"removeQuerySettings": q}) - except Exception: - pass +from .utils.setQuerySettings_common import cleanup_query_settings +# Property [Command Shape Acceptance]: accepts find, distinct, and aggregate shapes. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_find_shape(collection: Collection): @@ -55,7 +48,7 @@ def test_setQuerySettings_find_shape(collection: Collection): msg="should accept valid find shape", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -89,7 +82,7 @@ def test_setQuerySettings_distinct_shape(collection: Collection): msg="should accept valid distinct shape", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -122,9 +115,10 @@ def test_setQuerySettings_aggregate_shape(collection: Collection): msg="should accept valid aggregate shape", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) +# Property [Find Shape Variations]: setQuerySettings accepts find shapes with various field combos. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_find_filter_only(collection: Collection): @@ -155,7 +149,7 @@ def test_setQuerySettings_find_filter_only(collection: Collection): msg="should accept find with filter only", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -189,7 +183,7 @@ def test_setQuerySettings_find_filter_sort(collection: Collection): msg="should accept find with filter+sort", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -223,7 +217,7 @@ def test_setQuerySettings_find_filter_projection(collection: Collection): msg="should accept find with filter+projection", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -258,7 +252,7 @@ def test_setQuerySettings_find_filter_sort_projection(collection: Collection): msg="should accept find with all fields", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -292,7 +286,7 @@ def test_setQuerySettings_find_with_collation(collection: Collection): msg="should accept find with collation", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -326,7 +320,7 @@ def test_setQuerySettings_find_with_let(collection: Collection): msg="should accept find with let", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -360,9 +354,10 @@ def test_setQuerySettings_find_with_limit(collection: Collection): msg="should accept find with limit", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) +# Property [Distinct Shape Variations]: setQuerySettings accepts distinct shapes with query combos. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_distinct_key_only(collection: Collection): @@ -393,7 +388,7 @@ def test_setQuerySettings_distinct_key_only(collection: Collection): msg="should accept distinct key only", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -427,9 +422,10 @@ def test_setQuerySettings_distinct_complex_query(collection: Collection): msg="should accept distinct complex query", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) +# Property [Aggregate Shape Variations]: setQuerySettings accepts aggregate pipeline shapes. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_aggregate_match_only(collection: Collection): @@ -460,7 +456,7 @@ def test_setQuerySettings_aggregate_match_only(collection: Collection): msg="should accept aggregate $match only", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -493,7 +489,7 @@ def test_setQuerySettings_aggregate_match_group(collection: Collection): msg="should accept aggregate $match+$group", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -526,9 +522,10 @@ def test_setQuerySettings_aggregate_match_sort_limit(collection: Collection): msg="should accept aggregate $match+$sort+$limit", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) +# Property [$db Field Variations]: setQuerySettings accepts non-existent and special-char db names. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_db_nonexistent(collection: Collection): @@ -562,7 +559,7 @@ def test_setQuerySettings_db_nonexistent(collection: Collection): msg="should accept non-existent $db", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -595,4 +592,4 @@ def test_setQuerySettings_db_special_characters(collection: Collection): msg="should accept $db with special chars", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py index e6f98ac5c..faca7b2f1 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py @@ -10,24 +10,13 @@ import pytest from pymongo.collection import Collection -from documentdb_tests.framework.assertions import assertResult, assertSuccessPartial -from documentdb_tests.framework.error_codes import ( - QUERYSETTINGS_IDHACK_QUERY_ERROR, - QUERYSETTINGS_REJECT_ONLY_ERROR, -) +from documentdb_tests.framework.assertions import assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command - -def _cleanup(collection: Collection, queries: list[dict]) -> None: - """Remove all query settings created during the test.""" - admin = collection.database.client.admin - for q in queries: - try: - admin.command({"removeQuerySettings": q}) - except Exception: - pass +from .utils.setQuerySettings_common import cleanup_query_settings +# Property [indexHints Acceptance]: setQuerySettings accepts valid indexHints configurations. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_indexHints_single_index(collection: Collection): @@ -54,7 +43,7 @@ def test_setQuerySettings_indexHints_single_index(collection: Collection): ) assertSuccessPartial(result, {"ok": 1.0}, msg="should accept indexHints with single index") finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -87,7 +76,7 @@ def test_setQuerySettings_indexHints_multiple_indexes(collection: Collection): msg="should accept multiple indexes", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -116,38 +105,10 @@ def test_setQuerySettings_indexHints_key_pattern(collection: Collection): ) assertSuccessPartial(result, {"ok": 1.0}, msg="should accept indexHints with key pattern") finally: - _cleanup(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_indexHints_empty_allowed_rejected(collection: Collection): - """Test setQuerySettings rejects indexHints with empty allowedIndexes as empty settings.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"a4": 1}, - "$db": collection.database.name, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": [], - } - ], - }, - }, - ) - assertResult( - result, - error_code=QUERYSETTINGS_REJECT_ONLY_ERROR, - msg="should reject indexHints with empty allowedIndexes", - ) + cleanup_query_settings(collection, [query]) +# Property [reject Acceptance]: setQuerySettings accepts reject: true alone or with indexHints. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_reject_true(collection: Collection): @@ -167,7 +128,7 @@ def test_setQuerySettings_reject_true(collection: Collection): ) assertSuccessPartial(result, {"ok": 1.0}, msg="should accept settings with reject: true") finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -201,9 +162,10 @@ def test_setQuerySettings_reject_with_indexHints(collection: Collection): msg="should accept reject with indexHints", ) finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) +# Property [queryFramework Acceptance]: setQuerySettings accepts classic and sbe frameworks. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_queryFramework_classic(collection: Collection): @@ -231,7 +193,7 @@ def test_setQuerySettings_queryFramework_classic(collection: Collection): ) assertSuccessPartial(result, {"ok": 1.0}, msg="should accept queryFramework: classic") finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -261,9 +223,10 @@ def test_setQuerySettings_queryFramework_sbe(collection: Collection): ) assertSuccessPartial(result, {"ok": 1.0}, msg="should accept queryFramework: sbe") finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) +# Property [comment Acceptance]: setQuerySettings accepts the comment field. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_with_comment_string(collection: Collection): @@ -291,9 +254,10 @@ def test_setQuerySettings_with_comment_string(collection: Collection): ) assertSuccessPartial(result, {"ok": 1.0}, msg="should accept command with comment string") finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) +# Property [Update Behavior]: setQuerySettings can update existing settings by query or hash. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_update_existing_settings(collection: Collection): @@ -336,7 +300,7 @@ def test_setQuerySettings_update_existing_settings(collection: Collection): ) assertSuccessPartial(result, {"ok": 1.0}, msg="update setQuerySettings should succeed") finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) @pytest.mark.admin @@ -382,38 +346,10 @@ def test_setQuerySettings_update_via_hash(collection: Collection): ) assertSuccessPartial(result, {"ok": 1.0}, msg="update via hash should succeed") finally: - _cleanup(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_idhack_query_rejected(collection: Collection): - """Test setQuerySettings rejects queries eligible for IDHACK optimization.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"_id": 1}, - "$db": collection.database.name, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertResult( - result, - error_code=QUERYSETTINGS_IDHACK_QUERY_ERROR, - msg="setQuerySettings should reject IDHACK-eligible queries", - ) + cleanup_query_settings(collection, [query]) +# Property [Combined Settings]: setQuerySettings accepts all settings fields together. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_all_settings_combined(collection: Collection): @@ -442,4 +378,4 @@ def test_setQuerySettings_all_settings_combined(collection: Collection): ) assertSuccessPartial(result, {"ok": 1.0}, msg="should accept all settings combined") finally: - _cleanup(collection, [query]) + cleanup_query_settings(collection, [query]) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py index 39a01dc26..3d2c1566f 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py @@ -16,6 +16,7 @@ INVALID_NAMESPACE_ERROR, MISSING_FIELD_ERROR, QUERYSETTINGS_EMPTY_SETTINGS_ERROR, + QUERYSETTINGS_IDHACK_QUERY_ERROR, QUERYSETTINGS_INTERNAL_DB_ERROR, QUERYSETTINGS_NS_COLL_MISSING_ERROR, QUERYSETTINGS_NS_DB_MISSING_ERROR, @@ -27,6 +28,7 @@ from documentdb_tests.framework.executor import execute_admin_command +# Property [Query Shape Validation]: rejects malformed or unknown query shape documents. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_query_shape_missing_db(collection: Collection): @@ -113,6 +115,7 @@ def test_setQuerySettings_query_shape_unknown_command(collection: Collection): ) +# Property [Hash String Validation]: rejects invalid hash string formats. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_empty_hash_string(collection: Collection): @@ -138,6 +141,7 @@ def test_setQuerySettings_empty_hash_string(collection: Collection): ) +# Property [indexHints Structure Validation]: rejects indexHints missing required sub-fields. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_indexHints_missing_ns(collection: Collection): @@ -224,6 +228,7 @@ def test_setQuerySettings_indexHints_ns_missing_coll(collection: Collection): ) +# Property [Settings Value Validation]: rejects invalid field values in settings document. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_invalid_query_framework_value(collection: Collection): @@ -276,6 +281,7 @@ def test_setQuerySettings_reject_false_only(collection: Collection): ) +# Property [Settings Presence]: rejects missing or empty settings document. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_missing_settings(collection: Collection): @@ -319,6 +325,7 @@ def test_setQuerySettings_empty_settings(collection: Collection): ) +# Property [Unrecognized Fields]: rejects unknown top-level command fields. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_unrecognized_top_level_field(collection: Collection): @@ -349,6 +356,7 @@ def test_setQuerySettings_unrecognized_top_level_field(collection: Collection): ) +# Property [Database Restrictions]: rejects query shapes targeting internal databases. @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_system_collection(collection: Collection): @@ -405,3 +413,62 @@ def test_setQuerySettings_local_database(collection: Collection): error_code=QUERYSETTINGS_INTERNAL_DB_ERROR, msg="setQuerySettings should reject query shapes on local database", ) + + +# Property [indexHints Value Validation]: rejects empty allowedIndexes and IDHACK queries. +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_indexHints_empty_allowed_rejected(collection: Collection): + """Test setQuerySettings rejects indexHints with empty allowedIndexes.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"a4": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": [], + } + ], + }, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_REJECT_ONLY_ERROR, + msg="setQuerySettings should reject indexHints with empty allowedIndexes", + ) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_idhack_query_rejected(collection: Collection): + """Test setQuerySettings rejects queries eligible for IDHACK optimization.""" + result = execute_admin_command( + collection, + { + "setQuerySettings": { + "find": collection.name, + "filter": {"_id": 1}, + "$db": collection.database.name, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": collection.database.name, "coll": collection.name}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_IDHACK_QUERY_ERROR, + msg="setQuerySettings should reject IDHACK-eligible queries", + ) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/utils/__init__.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/utils/setQuerySettings_common.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/utils/setQuerySettings_common.py new file mode 100644 index 000000000..3ae8667c8 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/utils/setQuerySettings_common.py @@ -0,0 +1,25 @@ +"""Shared utilities for setQuerySettings tests.""" + +from __future__ import annotations + +from typing import Any + +from pymongo.collection import Collection + + +def cleanup_query_settings(collection: Collection, queries: list[dict]) -> None: + """Remove all query settings created during a test.""" + admin = collection.database.client.admin + for q in queries: + try: + admin.command({"removeQuerySettings": q}) + except Exception: + pass + + +def get_query_settings(collection: Collection) -> list[dict[str, Any]]: + """Retrieve all current query settings via $querySettings stage.""" + admin = collection.database.client.admin + result = admin.command({"aggregate": 1, "pipeline": [{"$querySettings": {}}], "cursor": {}}) + batch: list[dict[str, Any]] = result.get("cursor", {}).get("firstBatch", []) + return batch From c7f938c87ec5df2b2fa907cc615c4a4d6ce871af Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 12 Jun 2026 14:53:10 -0700 Subject: [PATCH 03/27] add AdminCommandTestCase Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_type_errors.py | 454 +++++++----------- .../tests/core/utils/command_test_case.py | 30 +- 2 files changed, 211 insertions(+), 273 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py index 45c9936c7..a45870db4 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py @@ -8,12 +8,14 @@ from __future__ import annotations from datetime import datetime, timezone -from typing import Any import pytest from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp -from pymongo.collection import Collection +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + AdminCommandTestCase, + CommandContext, +) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( FAILED_TO_PARSE_ERROR, @@ -21,351 +23,259 @@ TYPE_MISMATCH_ERROR, ) from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params -# Property [Primary Argument Type Rejection]: the setQuerySettings field must -# be a document (query shape) or string (hash). All other BSON types are -# rejected with TYPE_MISMATCH_ERROR. -_PRIMARY_ARG_INVALID_TYPES: list[tuple[str, Any]] = [ - ("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 [queryFramework Type Rejection]: the queryFramework field must be a -# string. Non-string BSON types are rejected with TYPE_MISMATCH_ERROR. -_QUERY_FRAMEWORK_INVALID_TYPES: list[tuple[str, Any]] = [ - ("int32", 42), - ("int64", Int64(42)), - ("double", 3.14), - ("decimal128", Decimal128("1")), - ("bool_true", True), - ("bool_false", False), - ("array", [1]), - ("object", {"k": "v"}), - ("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()), -] +# -- helpers ------------------------------------------------------------------ -# Property [reject Type Rejection]: the reject field must be a boolean. -# Non-boolean BSON types are rejected with TYPE_MISMATCH_ERROR. -_REJECT_INVALID_TYPES: list[tuple[str, Any]] = [ - ("null", None), - ("int32", 42), - ("int64", Int64(42)), - ("double", 3.14), - ("decimal128", Decimal128("1")), - ("string", "true"), - ("array", [True]), - ("object", {"k": "v"}), - ("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 [indexHints.ns.db Type Rejection]: the ns.db field must be a string. -_NS_DB_INVALID_TYPES: list[tuple[str, Any]] = [ - ("int32", 42), - ("bool", True), - ("array", ["test"]), - ("object", {"k": "v"}), -] +def _default_settings(ctx: CommandContext) -> dict: + """Build the standard indexHints settings block.""" + return { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + } -# Property [indexHints.ns.coll Type Rejection]: the ns.coll field must be a string. -_NS_COLL_INVALID_TYPES: list[tuple[str, Any]] = [ - ("int32", 42), - ("bool", True), -] -# Property [indexHints.allowedIndexes Type Rejection]: allowedIndexes must be an array. -_ALLOWED_INDEXES_INVALID_TYPES: list[tuple[str, Any]] = [ - ("string", "_id_"), - ("int32", 42), -] +def _default_query(ctx: CommandContext) -> dict: + """Build a minimal valid query shape.""" + return { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + } -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize( - "tid, value", - _PRIMARY_ARG_INVALID_TYPES, - ids=[t[0] for t in _PRIMARY_ARG_INVALID_TYPES], -) -def test_setQuerySettings_primary_arg_type_rejection(collection: Collection, tid: str, value: Any): - """Test setQuerySettings rejects invalid BSON types for the primary argument.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": value, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, +# Property [Primary Argument Type Rejection]: the setQuerySettings field must +# be a document (query shape) or string (hash). All other BSON types are +# rejected with TYPE_MISMATCH_ERROR. +SET_QUERY_SETTINGS_PRIMARY_ARG_TYPE_TESTS: list[AdminCommandTestCase] = [ + AdminCommandTestCase( + f"primary_arg_{tid}", + command=lambda ctx, v=value: { + "setQuerySettings": v, + "settings": _default_settings(ctx), }, - ) - assertResult( - result, error_code=TYPE_MISMATCH_ERROR, msg=f"setQuerySettings 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()), + ] +] - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize( - "tid, value", - _QUERY_FRAMEWORK_INVALID_TYPES, - ids=[t[0] for t in _QUERY_FRAMEWORK_INVALID_TYPES], -) -def test_setQuerySettings_query_framework_type_rejection( - collection: Collection, tid: str, value: Any -): - """Test setQuerySettings rejects invalid BSON types for queryFramework.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": value, - }, +# Property [queryFramework Type Rejection]: the queryFramework field must be a +# string. Non-string BSON types are rejected with TYPE_MISMATCH_ERROR. +SET_QUERY_SETTINGS_QUERY_FRAMEWORK_TYPE_TESTS: list[AdminCommandTestCase] = [ + AdminCommandTestCase( + f"query_framework_{tid}", + command=lambda ctx, v=value: { + "setQuerySettings": _default_query(ctx), + "settings": {**_default_settings(ctx), "queryFramework": v}, }, - ) - assertResult( - result, error_code=TYPE_MISMATCH_ERROR, msg=f"setQuerySettings should reject {tid} as queryFramework", ) + for tid, value in [ + ("int32", 42), + ("int64", Int64(42)), + ("double", 3.14), + ("decimal128", Decimal128("1")), + ("bool_true", True), + ("bool_false", False), + ("array", [1]), + ("object", {"k": "v"}), + ("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()), + ] +] - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize( - "tid, value", - _REJECT_INVALID_TYPES, - ids=[t[0] for t in _REJECT_INVALID_TYPES], -) -def test_setQuerySettings_reject_type_rejection(collection: Collection, tid: str, value: Any): - """Test setQuerySettings rejects invalid BSON types for reject field.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - "reject": value, - }, +# Property [reject Type Rejection]: the reject field must be a boolean. +# Non-boolean BSON types are rejected with TYPE_MISMATCH_ERROR. +SET_QUERY_SETTINGS_REJECT_TYPE_TESTS: list[AdminCommandTestCase] = [ + AdminCommandTestCase( + f"reject_{tid}", + command=lambda ctx, v=value: { + "setQuerySettings": _default_query(ctx), + "settings": {**_default_settings(ctx), "reject": v}, }, - ) - assertResult( - result, error_code=TYPE_MISMATCH_ERROR, msg=f"setQuerySettings should reject {tid} as reject field", ) + for tid, value in [ + ("null", None), + ("int32", 42), + ("int64", Int64(42)), + ("double", 3.14), + ("decimal128", Decimal128("1")), + ("string", "true"), + ("array", [True]), + ("object", {"k": "v"}), + ("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()), + ] +] - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize( - "tid, value", - _NS_DB_INVALID_TYPES, - ids=[t[0] for t in _NS_DB_INVALID_TYPES], -) -def test_setQuerySettings_ns_db_type_rejection(collection: Collection, tid: str, value: Any): - """Test setQuerySettings rejects invalid BSON types for indexHints.ns.db.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, +# Property [indexHints.ns.db Type Rejection]: the ns.db field must be a string. +SET_QUERY_SETTINGS_NS_DB_TYPE_TESTS: list[AdminCommandTestCase] = [ + AdminCommandTestCase( + f"ns_db_{tid}", + command=lambda ctx, v=value: { + "setQuerySettings": _default_query(ctx), "settings": { "indexHints": [ { - "ns": {"db": value, "coll": collection.name}, + "ns": {"db": v, "coll": ctx.collection}, "allowedIndexes": ["_id_"], } ], }, }, - ) - assertResult( - result, error_code=TYPE_MISMATCH_ERROR, msg=f"setQuerySettings should reject {tid} as indexHints.ns.db", ) + for tid, value in [ + ("int32", 42), + ("bool", True), + ("array", ["test"]), + ("object", {"k": "v"}), + ] +] - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize( - "tid, value", - _NS_COLL_INVALID_TYPES, - ids=[t[0] for t in _NS_COLL_INVALID_TYPES], -) -def test_setQuerySettings_ns_coll_type_rejection(collection: Collection, tid: str, value: Any): - """Test setQuerySettings rejects invalid BSON types for indexHints.ns.coll.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, +# Property [indexHints.ns.coll Type Rejection]: the ns.coll field must be a string. +SET_QUERY_SETTINGS_NS_COLL_TYPE_TESTS: list[AdminCommandTestCase] = [ + AdminCommandTestCase( + f"ns_coll_{tid}", + command=lambda ctx, v=value: { + "setQuerySettings": _default_query(ctx), "settings": { "indexHints": [ { - "ns": {"db": collection.database.name, "coll": value}, + "ns": {"db": ctx.database, "coll": v}, "allowedIndexes": ["_id_"], } ], }, }, - ) - assertResult( - result, error_code=TYPE_MISMATCH_ERROR, msg=f"setQuerySettings should reject {tid} as indexHints.ns.coll", ) + for tid, value in [ + ("int32", 42), + ("bool", True), + ] +] - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize( - "tid, value", - _ALLOWED_INDEXES_INVALID_TYPES, - ids=[t[0] for t in _ALLOWED_INDEXES_INVALID_TYPES], -) -def test_setQuerySettings_allowed_indexes_type_rejection( - collection: Collection, tid: str, value: Any -): - """Test setQuerySettings rejects invalid BSON types for indexHints.allowedIndexes.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, +# Property [indexHints.allowedIndexes Type Rejection]: allowedIndexes must be an array. +SET_QUERY_SETTINGS_ALLOWED_INDEXES_TYPE_TESTS: list[AdminCommandTestCase] = [ + AdminCommandTestCase( + f"allowed_indexes_{tid}", + command=lambda ctx, v=value: { + "setQuerySettings": _default_query(ctx), "settings": { "indexHints": [ { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": value, + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": v, } ], }, }, - ) - assertResult( - result, error_code=TYPE_MISMATCH_ERROR, msg=f"setQuerySettings should reject {tid} as indexHints.allowedIndexes", ) + for tid, value in [ + ("string", "_id_"), + ("int32", 42), + ] +] - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_allowed_indexes_null_missing(collection: Collection): - """Test setQuerySettings rejects null allowedIndexes as missing required field.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, +# Property [allowedIndexes null]: null allowedIndexes treated as missing required field. +SET_QUERY_SETTINGS_ALLOWED_INDEXES_EDGE_TESTS: list[AdminCommandTestCase] = [ + AdminCommandTestCase( + "allowed_indexes_null_missing", + command=lambda ctx: { + "setQuerySettings": _default_query(ctx), "settings": { "indexHints": [ { - "ns": {"db": collection.database.name, "coll": collection.name}, + "ns": {"db": ctx.database, "coll": ctx.collection}, "allowedIndexes": None, } ], }, }, - ) - assertResult( - result, error_code=MISSING_FIELD_ERROR, msg="setQuerySettings should reject null allowedIndexes as missing field", - ) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_allowed_indexes_non_string_element(collection: Collection): - """Test setQuerySettings rejects non-string elements in allowedIndexes array.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, + ), + AdminCommandTestCase( + "allowed_indexes_non_string_element", + command=lambda ctx: { + "setQuerySettings": _default_query(ctx), "settings": { "indexHints": [ { - "ns": {"db": collection.database.name, "coll": collection.name}, + "ns": {"db": ctx.database, "coll": ctx.collection}, "allowedIndexes": [42], } ], }, }, - ) - assertResult( - result, error_code=FAILED_TO_PARSE_ERROR, msg="setQuerySettings should reject non-string elements in allowedIndexes", + ), +] + +SET_QUERY_SETTINGS_TYPE_ERROR_TESTS: list[AdminCommandTestCase] = ( + SET_QUERY_SETTINGS_PRIMARY_ARG_TYPE_TESTS + + SET_QUERY_SETTINGS_QUERY_FRAMEWORK_TYPE_TESTS + + SET_QUERY_SETTINGS_REJECT_TYPE_TESTS + + SET_QUERY_SETTINGS_NS_DB_TYPE_TESTS + + SET_QUERY_SETTINGS_NS_COLL_TYPE_TESTS + + SET_QUERY_SETTINGS_ALLOWED_INDEXES_TYPE_TESTS + + SET_QUERY_SETTINGS_ALLOWED_INDEXES_EDGE_TESTS +) + + +@pytest.mark.admin +@pytest.mark.replica_set +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_TYPE_ERROR_TESTS)) +def test_setQuerySettings_type_errors(collection, test): + """Test setQuerySettings BSON type rejection.""" + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + error_code=test.error_code, + msg=test.msg, ) diff --git a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py index 8399464a6..0252e3764 100644 --- a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py +++ b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py @@ -1,4 +1,4 @@ -"""Shared test case for collection command tests.""" +"""Shared test case for collection and admin command tests.""" from __future__ import annotations @@ -118,3 +118,31 @@ def build_expected(self, ctx: CommandContext) -> dict[str, Any] | list[dict[str, if self.expected is None or isinstance(self.expected, (dict, list)): return self.expected return self.expected(ctx) + + +@dataclass(frozen=True) +class AdminCommandTestCase(CommandTestCase): + """Test case for admin-level commands (e.g. setQuerySettings). + + Admin commands run against the ``admin`` database via + ``execute_admin_command`` rather than against a specific collection's + database. They often need post-test cleanup (e.g. removing query + settings that were created). + + Attributes: + setup: Optional callable ``(Collection) -> None`` executed before + the command. Use for any prerequisite admin operations. + cleanup: Optional callable ``(CommandContext) -> list[dict]`` + returning admin commands to run after the test. Each dict + is passed to ``execute_admin_command`` inside a try/except + so cleanup failures are silently ignored. + """ + + setup: Callable[[Collection], Any] | None = None + cleanup: Callable[[CommandContext], list[dict[str, Any]]] | None = None + + def build_cleanup(self, ctx: CommandContext) -> list[dict[str, Any]]: + """Resolve cleanup commands from the callable, or return empty list.""" + if self.cleanup is None: + return [] + return self.cleanup(ctx) From fcd3ad489a233d66b9de9282043810891eb2b4c9 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 12 Jun 2026 14:57:37 -0700 Subject: [PATCH 04/27] convert more to use AdminCommandTestCase Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_behavior.py | 277 +++--- .../test_setQuerySettings_query_shapes.py | 865 +++++++----------- .../test_setQuerySettings_settings.py | 527 +++++------ ...test_setQuerySettings_validation_errors.py | 461 +++------- .../tests/core/utils/command_test_case.py | 14 +- 5 files changed, 843 insertions(+), 1301 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py index afa0db906..9f7bbb59a 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py @@ -10,91 +10,160 @@ import pytest from pymongo.collection import Collection +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + AdminCommandTestCase, + CommandContext, +) from documentdb_tests.framework.assertions import assertResult, assertSuccessPartial from documentdb_tests.framework.error_codes import QUERYSETTINGS_QUERY_REJECTED_ERROR from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.parametrize import pytest_params from .utils.setQuerySettings_common import cleanup_query_settings, get_query_settings +# -- helpers ------------------------------------------------------------------ + + +def _index_hints(ctx: CommandContext): + """Build a standard indexHints array for the fixture collection.""" + return [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ] + + +def _settings(ctx: CommandContext): + """Build a standard settings block with indexHints.""" + return {"indexHints": _index_hints(ctx)} + + +def _setup_setting(ctx: CommandContext, query: dict, settings: dict | None = None): + """Return a setup command list that creates a query setting.""" + return [{"setQuerySettings": query, "settings": settings or _settings(ctx)}] + + +def _cleanup_query(query_fn): + """Return a cleanup callable that removes the query shape built by query_fn.""" + return lambda ctx: [{"removeQuerySettings": query_fn(ctx)}] + + +def _find_query(ctx: CommandContext, field: str): + """Build a find query shape for the given field.""" + return {"find": ctx.collection, "filter": {field: 1}, "$db": ctx.database} + + +# -- Response Structure tests (single-step, fits AdminCommandTestCase) -------- # Property [Response Structure]: setQuerySettings response includes hash, query, and settings. +SET_QUERY_SETTINGS_RESPONSE_TESTS: list[AdminCommandTestCase] = [ + AdminCommandTestCase( + "response_contains_hash", + command=lambda ctx: { + "setQuerySettings": _find_query(ctx, "b1"), + "settings": _settings(ctx), + }, + expected={"ok": 1.0}, + cleanup=_cleanup_query(lambda ctx: _find_query(ctx, "b1")), + msg="response should contain queryShapeHash", + ), + AdminCommandTestCase( + "response_contains_representative_query", + command=lambda ctx: { + "setQuerySettings": _find_query(ctx, "b2"), + "settings": _settings(ctx), + }, + expected={"ok": 1.0}, + cleanup=_cleanup_query(lambda ctx: _find_query(ctx, "b2")), + msg="response should contain representativeQuery", + ), + AdminCommandTestCase( + "response_settings_echo", + command=lambda ctx: { + "setQuerySettings": _find_query(ctx, "b3"), + "settings": _settings(ctx), + }, + expected=lambda ctx: {"ok": 1.0, "settings": _settings(ctx)}, + cleanup=_cleanup_query(lambda ctx: _find_query(ctx, "b3")), + msg="response should echo applied settings", + ), +] + + @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_response_contains_hash(collection: Collection): - """Test setQuerySettings response contains queryShapeHash field.""" - query = { - "find": collection.name, - "filter": {"b1": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_RESPONSE_TESTS)) +def test_setQuerySettings_response(collection, test): + """Test setQuerySettings response structure.""" + ctx = CommandContext.from_collection(collection) try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertSuccessPartial( - result, - {"ok": 1.0, "queryShapeHash": result.get("queryShapeHash")}, - msg="response should contain queryShapeHash", - ) + result = execute_admin_command(collection, test.build_command(ctx)) + expected = test.build_expected(ctx) + # Also verify the dynamic fields are present + if test.id == "response_contains_hash": + expected["queryShapeHash"] = result.get("queryShapeHash") + elif test.id == "response_contains_representative_query": + expected["representativeQuery"] = result.get("representativeQuery") + assertSuccessPartial(result, expected, msg=test.msg) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass + + +# -- removeQuerySettings tests (multi-step: setup creates setting, command removes it) --- + +# Property [removeQuerySettings]: settings can be removed by query or hash. +SET_QUERY_SETTINGS_REMOVE_TESTS: list[AdminCommandTestCase] = [ + AdminCommandTestCase( + "removeQuerySettings_by_query", + setup_commands=lambda ctx: _setup_setting(ctx, _find_query(ctx, "b5")), + command=lambda ctx: {"removeQuerySettings": _find_query(ctx, "b5")}, + expected={"ok": 1.0}, + cleanup=_cleanup_query(lambda ctx: _find_query(ctx, "b5")), + msg="removeQuerySettings by query should succeed", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_response_contains_representative_query(collection: Collection): - """Test setQuerySettings response contains representativeQuery field.""" - query = { - "find": collection.name, - "filter": {"b2": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_REMOVE_TESTS)) +def test_setQuerySettings_remove(collection, test): + """Test removeQuerySettings removes settings.""" + ctx = CommandContext.from_collection(collection) try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertSuccessPartial( - result, - {"ok": 1.0, "representativeQuery": result.get("representativeQuery")}, - msg="response should contain representativeQuery", - ) + for cmd in test.build_setup(ctx): + execute_admin_command(collection, cmd) + result = execute_admin_command(collection, test.build_command(ctx)) + assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass + + +# -- Multi-step behavior tests (kept as individual functions) ----------------- +# Property [removeQuerySettings by hash]: requires capturing hash from setup result. @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_response_settings_echo(collection: Collection): - """Test setQuerySettings response echoes the settings that were applied.""" +def test_setQuerySettings_removeQuerySettings_by_hash(collection: Collection): + """Test removeQuerySettings removes settings by query shape hash.""" query = { "find": collection.name, - "filter": {"b3": 1}, + "filter": {"b6": 1}, "$db": collection.database.name, } try: - result = execute_admin_command( + # Setup: create a query setting and capture hash (no assertion — setup only) + setup_result = execute_admin_command( collection, { "setQuerySettings": query, @@ -108,21 +177,13 @@ def test_setQuerySettings_response_settings_echo(collection: Collection): }, }, ) - assertSuccessPartial( - result, - { - "ok": 1.0, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - msg="response should echo applied settings", + + query_hash = setup_result.get("queryShapeHash") + result = execute_admin_command( + collection, + {"removeQuerySettings": query_hash}, ) + assertSuccessPartial(result, {"ok": 1.0}, msg="removeQuerySettings by hash should succeed") finally: cleanup_query_settings(collection, [query]) @@ -166,78 +227,6 @@ def test_setQuerySettings_querySettings_stage_retrieval(collection: Collection): cleanup_query_settings(collection, [query]) -# Property [removeQuerySettings]: settings can be removed by query or hash. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_removeQuerySettings_by_query(collection: Collection): - """Test removeQuerySettings removes settings by representative query.""" - query = { - "find": collection.name, - "filter": {"b5": 1}, - "$db": collection.database.name, - } - try: - # Setup: create a query setting (no assertion — setup only) - execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - - result = execute_admin_command( - collection, - {"removeQuerySettings": query}, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="removeQuerySettings by query should succeed") - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_removeQuerySettings_by_hash(collection: Collection): - """Test removeQuerySettings removes settings by query shape hash.""" - query = { - "find": collection.name, - "filter": {"b6": 1}, - "$db": collection.database.name, - } - try: - # Setup: create a query setting and capture hash (no assertion — setup only) - setup_result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - - query_hash = setup_result.get("queryShapeHash") - result = execute_admin_command( - collection, - {"removeQuerySettings": query_hash}, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="removeQuerySettings by hash should succeed") - finally: - cleanup_query_settings(collection, [query]) - - # Property [Reject Blocks Query]: a rejected query returns an error when executed. @pytest.mark.admin @pytest.mark.replica_set diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py index 5d938749a..7897d85b0 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py @@ -8,588 +8,365 @@ from __future__ import annotations import pytest -from pymongo.collection import Collection +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + AdminCommandTestCase, + CommandContext, +) from documentdb_tests.framework.assertions import assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params -from .utils.setQuerySettings_common import cleanup_query_settings +# -- helpers ------------------------------------------------------------------ -# Property [Command Shape Acceptance]: accepts find, distinct, and aggregate shapes. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_find_shape(collection: Collection): - """Test setQuerySettings accepts a valid find query shape.""" - query = { - "find": collection.name, - "filter": {"x": 1}, - "sort": {"x": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept valid find shape", - ) - finally: - cleanup_query_settings(collection, [query]) +def _index_hints(ctx: CommandContext, db=None, coll=None): + """Build a standard indexHints array, optionally overriding db/coll.""" + return [ + { + "ns": {"db": db or ctx.database, "coll": coll or ctx.collection}, + "allowedIndexes": ["_id_"], + } + ] -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_distinct_shape(collection: Collection): - """Test setQuerySettings accepts a valid distinct query shape.""" - query = { - "distinct": collection.name, - "key": "x", - "query": {"x": {"$gt": 0}}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept valid distinct shape", - ) - finally: - cleanup_query_settings(collection, [query]) +def _settings(ctx: CommandContext, db=None, coll=None): + """Build a standard settings block with indexHints.""" + return {"indexHints": _index_hints(ctx, db=db, coll=coll)} -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_aggregate_shape(collection: Collection): - """Test setQuerySettings accepts a valid aggregate query shape.""" - query = { - "aggregate": collection.name, - "pipeline": [{"$match": {"x": 1}}], - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept valid aggregate shape", - ) - finally: - cleanup_query_settings(collection, [query]) +def _cleanup(query: dict): + """Return a cleanup callable that removes the given query shape.""" + return lambda ctx: [{"removeQuerySettings": query}] -# Property [Find Shape Variations]: setQuerySettings accepts find shapes with various field combos. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_find_filter_only(collection: Collection): - """Test setQuerySettings accepts find shape with only filter, no sort or projection.""" - query = { - "find": collection.name, - "filter": {"a": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept find with filter only", - ) - finally: - cleanup_query_settings(collection, [query]) +# -- test case helpers -------------------------------------------------------- -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_find_filter_sort(collection: Collection): - """Test setQuerySettings accepts find shape with filter and sort.""" - query = { - "find": collection.name, - "filter": {"b": 1}, - "sort": {"b": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept find with filter+sort", - ) - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_find_filter_projection(collection: Collection): - """Test setQuerySettings accepts find shape with filter and projection.""" - query = { - "find": collection.name, - "filter": {"c": 1}, - "projection": {"c": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept find with filter+projection", - ) - finally: - cleanup_query_settings(collection, [query]) +def _find_case(tid, query_fn, msg): + """Build an AdminCommandTestCase for a find query shape.""" + return AdminCommandTestCase( + tid, + command=lambda ctx, qf=query_fn: { + "setQuerySettings": qf(ctx), + "settings": _settings(ctx), + }, + expected={"ok": 1.0}, + cleanup=lambda ctx, qf=query_fn: [{"removeQuerySettings": qf(ctx)}], + msg=msg, + ) -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_find_filter_sort_projection(collection: Collection): - """Test setQuerySettings accepts find shape with filter, sort, and projection.""" - query = { - "find": collection.name, - "filter": {"d": 1}, - "sort": {"d": 1}, - "projection": {"d": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, +# Property [Command Shape Acceptance]: accepts find, distinct, and aggregate shapes. +# Property [Find Shape Variations]: setQuerySettings accepts find shapes with various field combos. +# Property [Distinct Shape Variations]: setQuerySettings accepts distinct shapes with query combos. +# Property [Aggregate Shape Variations]: setQuerySettings accepts aggregate pipeline shapes. +# Property [$db Field Variations]: setQuerySettings accepts non-existent and special-char db names. +SET_QUERY_SETTINGS_QUERY_SHAPE_TESTS: list[AdminCommandTestCase] = [ + # -- Command shape acceptance -- + _find_case( + "find_shape", + lambda ctx: { + "find": ctx.collection, + "filter": {"x": 1}, + "sort": {"x": 1}, + "$db": ctx.database, + }, + msg="should accept valid find shape", + ), + AdminCommandTestCase( + "distinct_shape", + command=lambda ctx: { + "setQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "query": {"x": {"$gt": 0}}, + "$db": ctx.database, }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept find with all fields", - ) - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_find_with_collation(collection: Collection): - """Test setQuerySettings accepts find shape with collation.""" - query = { - "find": collection.name, - "filter": {"e": "abc"}, - "collation": {"locale": "en", "strength": 2}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": _settings(ctx), + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "query": {"x": {"$gt": 0}}, + "$db": ctx.database, + } + } + ], + msg="should accept valid distinct shape", + ), + AdminCommandTestCase( + "aggregate_shape", + command=lambda ctx: { + "setQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"x": 1}}], + "$db": ctx.database, }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept find with collation", - ) - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_find_with_let(collection: Collection): - """Test setQuerySettings accepts find shape with let variables.""" - query = { - "find": collection.name, - "filter": {"$expr": {"$eq": ["$f", "$$target"]}}, - "let": {"target": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": _settings(ctx), + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"x": 1}}], + "$db": ctx.database, + } + } + ], + msg="should accept valid aggregate shape", + ), + # -- Find shape variations -- + _find_case( + "find_filter_only", + lambda ctx: {"find": ctx.collection, "filter": {"a": 1}, "$db": ctx.database}, + msg="should accept find with filter only", + ), + _find_case( + "find_filter_sort", + lambda ctx: { + "find": ctx.collection, + "filter": {"b": 1}, + "sort": {"b": 1}, + "$db": ctx.database, + }, + msg="should accept find with filter+sort", + ), + _find_case( + "find_filter_projection", + lambda ctx: { + "find": ctx.collection, + "filter": {"c": 1}, + "projection": {"c": 1}, + "$db": ctx.database, + }, + msg="should accept find with filter+projection", + ), + _find_case( + "find_filter_sort_projection", + lambda ctx: { + "find": ctx.collection, + "filter": {"d": 1}, + "sort": {"d": 1}, + "projection": {"d": 1}, + "$db": ctx.database, + }, + msg="should accept find with all fields", + ), + _find_case( + "find_with_collation", + lambda ctx: { + "find": ctx.collection, + "filter": {"e": "abc"}, + "collation": {"locale": "en", "strength": 2}, + "$db": ctx.database, + }, + msg="should accept find with collation", + ), + _find_case( + "find_with_let", + lambda ctx: { + "find": ctx.collection, + "filter": {"$expr": {"$eq": ["$f", "$$target"]}}, + "let": {"target": 1}, + "$db": ctx.database, + }, + msg="should accept find with let", + ), + _find_case( + "find_with_limit", + lambda ctx: { + "find": ctx.collection, + "filter": {"g": 1}, + "limit": 10, + "$db": ctx.database, + }, + msg="should accept find with limit", + ), + # -- Distinct shape variations -- + AdminCommandTestCase( + "distinct_key_only", + command=lambda ctx: { + "setQuerySettings": { + "distinct": ctx.collection, + "key": "j", + "$db": ctx.database, }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept find with let", - ) - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_find_with_limit(collection: Collection): - """Test setQuerySettings accepts find shape containing limit.""" - query = { - "find": collection.name, - "filter": {"g": 1}, - "limit": 10, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": _settings(ctx), + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "j", + "$db": ctx.database, + } + } + ], + msg="should accept distinct key only", + ), + AdminCommandTestCase( + "distinct_complex_query", + command=lambda ctx: { + "setQuerySettings": { + "distinct": ctx.collection, + "key": "k", + "query": {"$and": [{"k": {"$gt": 0}}, {"k": {"$lt": 100}}]}, + "$db": ctx.database, }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept find with limit", - ) - finally: - cleanup_query_settings(collection, [query]) - - -# Property [Distinct Shape Variations]: setQuerySettings accepts distinct shapes with query combos. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_distinct_key_only(collection: Collection): - """Test setQuerySettings accepts distinct shape with key only, no query filter.""" - query = { - "distinct": collection.name, - "key": "j", - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": _settings(ctx), + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "k", + "query": {"$and": [{"k": {"$gt": 0}}, {"k": {"$lt": 100}}]}, + "$db": ctx.database, + } + } + ], + msg="should accept distinct complex query", + ), + # -- Aggregate shape variations -- + AdminCommandTestCase( + "aggregate_match_only", + command=lambda ctx: { + "setQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"l": 1}}], + "$db": ctx.database, }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept distinct key only", - ) - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_distinct_complex_query(collection: Collection): - """Test setQuerySettings accepts distinct shape with complex query filter.""" - query = { - "distinct": collection.name, - "key": "k", - "query": {"$and": [{"k": {"$gt": 0}}, {"k": {"$lt": 100}}]}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": _settings(ctx), + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"l": 1}}], + "$db": ctx.database, + } + } + ], + msg="should accept aggregate $match only", + ), + AdminCommandTestCase( + "aggregate_match_group", + command=lambda ctx: { + "setQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [ + {"$match": {"m": 1}}, + {"$group": {"_id": "$m", "count": {"$sum": 1}}}, + ], + "$db": ctx.database, }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept distinct complex query", - ) - finally: - cleanup_query_settings(collection, [query]) - - -# Property [Aggregate Shape Variations]: setQuerySettings accepts aggregate pipeline shapes. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_aggregate_match_only(collection: Collection): - """Test setQuerySettings accepts aggregate shape with single $match stage.""" - query = { - "aggregate": collection.name, - "pipeline": [{"$match": {"l": 1}}], - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": _settings(ctx), + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [ + {"$match": {"m": 1}}, + {"$group": {"_id": "$m", "count": {"$sum": 1}}}, ], - }, + "$db": ctx.database, + } + } + ], + msg="should accept aggregate $match+$group", + ), + AdminCommandTestCase( + "aggregate_match_sort_limit", + command=lambda ctx: { + "setQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"n": 1}}, {"$sort": {"n": 1}}, {"$limit": 5}], + "$db": ctx.database, }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept aggregate $match only", - ) - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_aggregate_match_group(collection: Collection): - """Test setQuerySettings accepts aggregate shape with $match and $group pipeline.""" - query = { - "aggregate": collection.name, - "pipeline": [{"$match": {"m": 1}}, {"$group": {"_id": "$m", "count": {"$sum": 1}}}], - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": _settings(ctx), + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"n": 1}}, {"$sort": {"n": 1}}, {"$limit": 5}], + "$db": ctx.database, + } + } + ], + msg="should accept aggregate $match+$sort+$limit", + ), + # -- $db field variations -- + AdminCommandTestCase( + "db_nonexistent", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"o": 1}, + "$db": "nonexistent_db_for_query_settings_test", }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept aggregate $match+$group", - ) - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_aggregate_match_sort_limit(collection: Collection): - """Test setQuerySettings accepts aggregate shape with $match, $sort, and $limit.""" - query = { - "aggregate": collection.name, - "pipeline": [{"$match": {"n": 1}}, {"$sort": {"n": 1}}, {"$limit": 5}], - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": _settings(ctx, db="nonexistent_db_for_query_settings_test"), + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"o": 1}, + "$db": "nonexistent_db_for_query_settings_test", + } + } + ], + msg="should accept non-existent $db", + ), + AdminCommandTestCase( + "db_special_characters", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"p": 1}, + "$db": "test-special-db", }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept aggregate $match+$sort+$limit", - ) - finally: - cleanup_query_settings(collection, [query]) - - -# Property [$db Field Variations]: setQuerySettings accepts non-existent and special-char db names. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_db_nonexistent(collection: Collection): - """Test setQuerySettings accepts $db pointing to a non-existent database.""" - query = { - "find": collection.name, - "filter": {"o": 1}, - "$db": "nonexistent_db_for_query_settings_test", - } - try: - result = execute_admin_command( - collection, + "settings": _settings(ctx, db="test-special-db"), + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": { - "db": "nonexistent_db_for_query_settings_test", - "coll": collection.name, - }, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept non-existent $db", - ) - finally: - cleanup_query_settings(collection, [query]) + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"p": 1}, + "$db": "test-special-db", + } + } + ], + msg="should accept $db with special chars", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_db_special_characters(collection: Collection): - """Test setQuerySettings accepts $db with special characters like hyphens.""" - query = { - "find": collection.name, - "filter": {"p": 1}, - "$db": "test-special-db", - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_QUERY_SHAPE_TESTS)) +def test_setQuerySettings_query_shapes(collection, test): + """Test setQuerySettings accepts valid query shapes.""" + ctx = CommandContext.from_collection(collection) try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": "test-special-db", "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept $db with special chars", - ) + result = execute_admin_command(collection, test.build_command(ctx)) + assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) finally: - cleanup_query_settings(collection, [query]) + 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/setQuerySettings/test_setQuerySettings_settings.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py index faca7b2f1..c0f41a7b4 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py @@ -8,264 +8,272 @@ from __future__ import annotations import pytest -from pymongo.collection import Collection +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + AdminCommandTestCase, + CommandContext, +) from documentdb_tests.framework.assertions import assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params from .utils.setQuerySettings_common import cleanup_query_settings +# -- helpers ------------------------------------------------------------------ + + +def _index_hints(ctx: CommandContext, allowed=None): + """Build a standard indexHints array for the fixture collection.""" + return [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": allowed or ["_id_"], + } + ] + # Property [indexHints Acceptance]: setQuerySettings accepts valid indexHints configurations. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_indexHints_single_index(collection: Collection): - """Test setQuerySettings accepts indexHints with a single named index.""" - query = { - "find": collection.name, - "filter": {"a1": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, +# Property [reject Acceptance]: setQuerySettings accepts reject: true alone or with indexHints. +# Property [queryFramework Acceptance]: setQuerySettings accepts classic and sbe frameworks. +# Property [comment Acceptance]: setQuerySettings accepts the comment field. +# Property [Combined Settings]: setQuerySettings accepts all settings fields together. +SET_QUERY_SETTINGS_SETTINGS_TESTS: list[AdminCommandTestCase] = [ + AdminCommandTestCase( + "indexHints_single_index", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a1": 1}, + "$db": ctx.database, + }, + "settings": {"indexHints": _index_hints(ctx)}, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a1": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept indexHints with single index", + ), + AdminCommandTestCase( + "indexHints_multiple_indexes", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a2": 1}, + "$db": ctx.database, }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="should accept indexHints with single index") - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_indexHints_multiple_indexes(collection: Collection): - """Test setQuerySettings accepts indexHints with multiple allowedIndexes entries.""" - query = { - "find": collection.name, - "filter": {"a2": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": {"indexHints": _index_hints(ctx, ["_id_", {"a2": 1}])}, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_", {"a2": 1}], - } - ], - }, + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a2": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept multiple indexes", + ), + AdminCommandTestCase( + "indexHints_key_pattern", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a3": 1}, + "$db": ctx.database, }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept multiple indexes", - ) - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_indexHints_key_pattern(collection: Collection): - """Test setQuerySettings accepts indexHints with index key pattern instead of name.""" - query = { - "find": collection.name, - "filter": {"a3": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": {"indexHints": _index_hints(ctx, [{"a3": 1}])}, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": [{"a3": 1}], - } - ], - }, + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a3": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept indexHints with key pattern", + ), + AdminCommandTestCase( + "reject_true", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a5": 1}, + "$db": ctx.database, }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="should accept indexHints with key pattern") - finally: - cleanup_query_settings(collection, [query]) - - -# Property [reject Acceptance]: setQuerySettings accepts reject: true alone or with indexHints. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_reject_true(collection: Collection): - """Test setQuerySettings accepts settings with reject: true.""" - query = { - "find": collection.name, - "filter": {"a5": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": {"reject": True}, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": {"reject": True}, + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a5": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept settings with reject: true", + ), + AdminCommandTestCase( + "reject_with_indexHints", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a6": 1}, + "$db": ctx.database, }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="should accept settings with reject: true") - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_reject_with_indexHints(collection: Collection): - """Test setQuerySettings accepts settings with both reject and indexHints.""" - query = { - "find": collection.name, - "filter": {"a6": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": {"indexHints": _index_hints(ctx), "reject": True}, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - "reject": True, - }, + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a6": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept reject with indexHints", + ), + AdminCommandTestCase( + "queryFramework_classic", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a7": 1}, + "$db": ctx.database, }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="should accept reject with indexHints", - ) - finally: - cleanup_query_settings(collection, [query]) - - -# Property [queryFramework Acceptance]: setQuerySettings accepts classic and sbe frameworks. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_queryFramework_classic(collection: Collection): - """Test setQuerySettings accepts queryFramework: classic.""" - query = { - "find": collection.name, - "filter": {"a7": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": {"indexHints": _index_hints(ctx), "queryFramework": "classic"}, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": "classic", - }, + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a7": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept queryFramework: classic", + ), + AdminCommandTestCase( + "queryFramework_sbe", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a8": 1}, + "$db": ctx.database, }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="should accept queryFramework: classic") - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_queryFramework_sbe(collection: Collection): - """Test setQuerySettings accepts queryFramework: sbe.""" - query = { - "find": collection.name, - "filter": {"a8": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, + "settings": {"indexHints": _index_hints(ctx), "queryFramework": "sbe"}, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": "sbe", - }, + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a8": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept queryFramework: sbe", + ), + AdminCommandTestCase( + "with_comment_string", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a9": 1}, + "$db": ctx.database, }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="should accept queryFramework: sbe") - finally: - cleanup_query_settings(collection, [query]) + "settings": {"indexHints": _index_hints(ctx)}, + "comment": "test comment for setQuerySettings", + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a9": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept command with comment string", + ), + AdminCommandTestCase( + "all_settings_combined", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a12": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": _index_hints(ctx), + "queryFramework": "classic", + "reject": True, + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a12": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept all settings combined", + ), +] -# Property [comment Acceptance]: setQuerySettings accepts the comment field. @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_with_comment_string(collection: Collection): - """Test setQuerySettings accepts a comment field with string value.""" - query = { - "find": collection.name, - "filter": {"a9": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_SETTINGS_TESTS)) +def test_setQuerySettings_settings(collection, test): + """Test setQuerySettings accepts valid settings configurations.""" + ctx = CommandContext.from_collection(collection) try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - "comment": "test comment for setQuerySettings", - }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="should accept command with comment string") + result = execute_admin_command(collection, test.build_command(ctx)) + assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass + + +# -- Update Behavior tests (multi-step, kept as individual functions) --------- # Property [Update Behavior]: setQuerySettings can update existing settings by query or hash. @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_update_existing_settings(collection: Collection): +def test_setQuerySettings_update_existing_settings(collection): """Test setQuerySettings can update settings for an existing query shape.""" + ctx = CommandContext.from_collection(collection) query = { - "find": collection.name, + "find": ctx.collection, "filter": {"a10": 1}, - "$db": collection.database.name, + "$db": ctx.database, } try: # Setup: create initial settings (no assertion — setup only) @@ -273,14 +281,7 @@ def test_setQuerySettings_update_existing_settings(collection: Collection): collection, { "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "settings": {"indexHints": _index_hints(ctx)}, }, ) @@ -288,14 +289,7 @@ def test_setQuerySettings_update_existing_settings(collection: Collection): collection, { "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_", {"a10": 1}], - } - ], - }, + "settings": {"indexHints": _index_hints(ctx, ["_id_", {"a10": 1}])}, }, ) assertSuccessPartial(result, {"ok": 1.0}, msg="update setQuerySettings should succeed") @@ -305,12 +299,13 @@ def test_setQuerySettings_update_existing_settings(collection: Collection): @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_update_via_hash(collection: Collection): +def test_setQuerySettings_update_via_hash(collection): """Test setQuerySettings can update settings using the query shape hash.""" + ctx = CommandContext.from_collection(collection) query = { - "find": collection.name, + "find": ctx.collection, "filter": {"a11": 1}, - "$db": collection.database.name, + "$db": ctx.database, } try: # Setup: create initial settings and capture hash (no assertion — setup only) @@ -318,14 +313,7 @@ def test_setQuerySettings_update_via_hash(collection: Collection): collection, { "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "settings": {"indexHints": _index_hints(ctx)}, }, ) @@ -334,48 +322,9 @@ def test_setQuerySettings_update_via_hash(collection: Collection): collection, { "setQuerySettings": query_hash, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_", {"a11": 1}], - } - ], - }, + "settings": {"indexHints": _index_hints(ctx, ["_id_", {"a11": 1}])}, }, ) assertSuccessPartial(result, {"ok": 1.0}, msg="update via hash should succeed") finally: cleanup_query_settings(collection, [query]) - - -# Property [Combined Settings]: setQuerySettings accepts all settings fields together. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_all_settings_combined(collection: Collection): - """Test setQuerySettings accepts all settings fields combined.""" - query = { - "find": collection.name, - "filter": {"a12": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": "classic", - "reject": True, - }, - }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="should accept all settings combined") - finally: - cleanup_query_settings(collection, [query]) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py index 3d2c1566f..04cb4c811 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py @@ -8,8 +8,11 @@ from __future__ import annotations import pytest -from pymongo.collection import Collection +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + AdminCommandTestCase, + CommandContext, +) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( BAD_VALUE_ERROR, @@ -26,134 +29,92 @@ UNRECOGNIZED_COMMAND_FIELD_ERROR, ) from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +# -- helpers ------------------------------------------------------------------ + + +def _default_settings(ctx: CommandContext) -> dict: + """Build the standard indexHints settings block.""" + return { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + } + + +def _default_query(ctx: CommandContext) -> dict: + """Build a minimal valid query shape.""" + return { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + } # Property [Query Shape Validation]: rejects malformed or unknown query shape documents. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_query_shape_missing_db(collection: Collection): - """Test setQuerySettings rejects a query shape document missing $db field.""" - result = execute_admin_command( - collection, - { +# Property [Hash String Validation]: rejects invalid hash string formats. +# Property [indexHints Structure Validation]: rejects indexHints missing required sub-fields. +# Property [Settings Value Validation]: rejects invalid field values in settings document. +# Property [Settings Presence]: rejects missing or empty settings document. +# Property [Unrecognized Fields]: rejects unknown top-level command fields. +# Property [Database Restrictions]: rejects query shapes targeting internal databases. +# Property [indexHints Value Validation]: rejects empty allowedIndexes and IDHACK queries. +SET_QUERY_SETTINGS_VALIDATION_ERROR_TESTS: list[AdminCommandTestCase] = [ + AdminCommandTestCase( + "query_shape_missing_db", + command=lambda ctx: { "setQuerySettings": { - "find": collection.name, + "find": ctx.collection, "filter": {"x": 1}, }, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "settings": _default_settings(ctx), }, - ) - assertResult( - result, error_code=MISSING_FIELD_ERROR, msg="setQuerySettings should reject query shape missing $db field", - ) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_query_shape_empty_db(collection: Collection): - """Test setQuerySettings rejects a query shape with empty string $db.""" - result = execute_admin_command( - collection, - { + ), + AdminCommandTestCase( + "query_shape_empty_db", + command=lambda ctx: { "setQuerySettings": { - "find": collection.name, + "find": ctx.collection, "filter": {"x": 1}, "$db": "", }, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "settings": _default_settings(ctx), }, - ) - assertResult( - result, error_code=INVALID_NAMESPACE_ERROR, msg="setQuerySettings should reject query shape with empty $db", - ) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_query_shape_unknown_command(collection: Collection): - """Test setQuerySettings rejects a query shape with an unknown command type.""" - result = execute_admin_command( - collection, - { + ), + AdminCommandTestCase( + "query_shape_unknown_command", + command=lambda ctx: { "setQuerySettings": { - "unknownCommand": collection.name, + "unknownCommand": ctx.collection, "filter": {"x": 1}, - "$db": collection.database.name, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], + "$db": ctx.database, }, + "settings": _default_settings(ctx), }, - ) - assertResult( - result, error_code=QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, msg="setQuerySettings should reject unknown command type in query shape", - ) - - -# Property [Hash String Validation]: rejects invalid hash string formats. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_empty_hash_string(collection: Collection): - """Test setQuerySettings rejects an empty hash string.""" - result = execute_admin_command( - collection, - { + ), + AdminCommandTestCase( + "empty_hash_string", + command=lambda ctx: { "setQuerySettings": "", - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + "settings": _default_settings(ctx), }, - ) - assertResult( - result, error_code=INVALID_LENGTH_ERROR, msg="setQuerySettings should reject empty hash string", - ) - - -# Property [indexHints Structure Validation]: rejects indexHints missing required sub-fields. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_indexHints_missing_ns(collection: Collection): - """Test setQuerySettings rejects indexHints entry missing ns field.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, + ), + AdminCommandTestCase( + "indexHints_missing_ns", + command=lambda ctx: { + "setQuerySettings": _default_query(ctx), "settings": { "indexHints": [ { @@ -162,208 +123,89 @@ def test_setQuerySettings_indexHints_missing_ns(collection: Collection): ], }, }, - ) - assertResult( - result, error_code=MISSING_FIELD_ERROR, msg="setQuerySettings should reject indexHints missing ns field", - ) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_indexHints_ns_missing_db(collection: Collection): - """Test setQuerySettings rejects indexHints.ns missing db field.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, + ), + AdminCommandTestCase( + "indexHints_ns_missing_db", + command=lambda ctx: { + "setQuerySettings": _default_query(ctx), "settings": { "indexHints": [ { - "ns": {"coll": collection.name}, + "ns": {"coll": ctx.collection}, "allowedIndexes": ["_id_"], } ], }, }, - ) - assertResult( - result, error_code=QUERYSETTINGS_NS_DB_MISSING_ERROR, msg="setQuerySettings should reject indexHints.ns missing db field", - ) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_indexHints_ns_missing_coll(collection: Collection): - """Test setQuerySettings rejects indexHints.ns missing coll field.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, + ), + AdminCommandTestCase( + "indexHints_ns_missing_coll", + command=lambda ctx: { + "setQuerySettings": _default_query(ctx), "settings": { "indexHints": [ { - "ns": {"db": collection.database.name}, + "ns": {"db": ctx.database}, "allowedIndexes": ["_id_"], } ], }, }, - ) - assertResult( - result, error_code=QUERYSETTINGS_NS_COLL_MISSING_ERROR, msg="setQuerySettings should reject indexHints.ns missing coll field", - ) - - -# Property [Settings Value Validation]: rejects invalid field values in settings document. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_invalid_query_framework_value(collection: Collection): - """Test setQuerySettings rejects an invalid queryFramework string value.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": "invalidFramework", - }, + ), + AdminCommandTestCase( + "invalid_query_framework_value", + command=lambda ctx: { + "setQuerySettings": _default_query(ctx), + "settings": {**_default_settings(ctx), "queryFramework": "invalidFramework"}, }, - ) - assertResult( - result, error_code=BAD_VALUE_ERROR, msg="setQuerySettings should reject invalid queryFramework string", - ) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_reject_false_only(collection: Collection): - """Test setQuerySettings rejects settings with only reject: false and no other settings.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, + ), + AdminCommandTestCase( + "reject_false_only", + command=lambda ctx: { + "setQuerySettings": _default_query(ctx), "settings": {"reject": False}, }, - ) - assertResult( - result, error_code=QUERYSETTINGS_REJECT_ONLY_ERROR, msg="setQuerySettings should reject settings with only reject: false", - ) - - -# Property [Settings Presence]: rejects missing or empty settings document. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_missing_settings(collection: Collection): - """Test setQuerySettings rejects command missing the settings field entirely.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, + ), + AdminCommandTestCase( + "missing_settings", + command=lambda ctx: { + "setQuerySettings": _default_query(ctx), }, - ) - assertResult( - result, error_code=MISSING_FIELD_ERROR, msg="setQuerySettings should reject missing settings field", - ) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_empty_settings(collection: Collection): - """Test setQuerySettings rejects empty settings document.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, + ), + AdminCommandTestCase( + "empty_settings", + command=lambda ctx: { + "setQuerySettings": _default_query(ctx), "settings": {}, }, - ) - assertResult( - result, error_code=QUERYSETTINGS_EMPTY_SETTINGS_ERROR, msg="setQuerySettings should reject empty settings document", - ) - - -# Property [Unrecognized Fields]: rejects unknown top-level command fields. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_unrecognized_top_level_field(collection: Collection): - """Test setQuerySettings rejects unrecognized top-level fields.""" - result = execute_admin_command( - collection, - { - "setQuerySettings": { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, + ), + AdminCommandTestCase( + "unrecognized_top_level_field", + command=lambda ctx: { + "setQuerySettings": _default_query(ctx), + "settings": _default_settings(ctx), "unknownField": 1, }, - ) - assertResult( - result, error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, msg="setQuerySettings should reject unrecognized top-level field", - ) - - -# Property [Database Restrictions]: rejects query shapes targeting internal databases. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_system_collection(collection: Collection): - """Test setQuerySettings rejects query shapes targeting internal databases.""" - result = execute_admin_command( - collection, - { + ), + AdminCommandTestCase( + "system_collection", + command=lambda ctx: { "setQuerySettings": { "find": "system.users", "filter": {}, @@ -378,21 +220,12 @@ def test_setQuerySettings_system_collection(collection: Collection): ], }, }, - ) - assertResult( - result, error_code=QUERYSETTINGS_INTERNAL_DB_ERROR, msg="setQuerySettings should reject query shapes on internal databases", - ) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_local_database(collection: Collection): - """Test setQuerySettings rejects query shapes targeting local database.""" - result = execute_admin_command( - collection, - { + ), + AdminCommandTestCase( + "local_database", + command=lambda ctx: { "setQuerySettings": { "find": "oplog.rs", "filter": {}, @@ -407,68 +240,54 @@ def test_setQuerySettings_local_database(collection: Collection): ], }, }, - ) - assertResult( - result, error_code=QUERYSETTINGS_INTERNAL_DB_ERROR, msg="setQuerySettings should reject query shapes on local database", - ) - - -# Property [indexHints Value Validation]: rejects empty allowedIndexes and IDHACK queries. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_indexHints_empty_allowed_rejected(collection: Collection): - """Test setQuerySettings rejects indexHints with empty allowedIndexes.""" - result = execute_admin_command( - collection, - { + ), + AdminCommandTestCase( + "indexHints_empty_allowed_rejected", + command=lambda ctx: { "setQuerySettings": { - "find": collection.name, + "find": ctx.collection, "filter": {"a4": 1}, - "$db": collection.database.name, + "$db": ctx.database, }, "settings": { "indexHints": [ { - "ns": {"db": collection.database.name, "coll": collection.name}, + "ns": {"db": ctx.database, "coll": ctx.collection}, "allowedIndexes": [], } ], }, }, - ) - assertResult( - result, error_code=QUERYSETTINGS_REJECT_ONLY_ERROR, msg="setQuerySettings should reject indexHints with empty allowedIndexes", - ) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_idhack_query_rejected(collection: Collection): - """Test setQuerySettings rejects queries eligible for IDHACK optimization.""" - result = execute_admin_command( - collection, - { + ), + AdminCommandTestCase( + "idhack_query_rejected", + command=lambda ctx: { "setQuerySettings": { - "find": collection.name, + "find": ctx.collection, "filter": {"_id": 1}, - "$db": collection.database.name, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], + "$db": ctx.database, }, + "settings": _default_settings(ctx), }, - ) - assertResult( - result, error_code=QUERYSETTINGS_IDHACK_QUERY_ERROR, msg="setQuerySettings should reject IDHACK-eligible queries", + ), +] + + +@pytest.mark.admin +@pytest.mark.replica_set +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_VALIDATION_ERROR_TESTS)) +def test_setQuerySettings_validation_errors(collection, test): + """Test setQuerySettings structural and validation error rejection.""" + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + error_code=test.error_code, + msg=test.msg, ) diff --git a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py index 0252e3764..e31147db4 100644 --- a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py +++ b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py @@ -130,17 +130,25 @@ class AdminCommandTestCase(CommandTestCase): settings that were created). Attributes: - setup: Optional callable ``(Collection) -> None`` executed before - the command. Use for any prerequisite admin operations. + setup_commands: Optional callable ``(CommandContext) -> list[dict]`` + returning admin commands to execute **before** the main + command. Use for prerequisite operations such as creating + a query setting before testing removal. cleanup: Optional callable ``(CommandContext) -> list[dict]`` returning admin commands to run after the test. Each dict is passed to ``execute_admin_command`` inside a try/except so cleanup failures are silently ignored. """ - setup: Callable[[Collection], Any] | None = None + setup_commands: Callable[[CommandContext], list[dict[str, Any]]] | None = None cleanup: Callable[[CommandContext], list[dict[str, Any]]] | None = None + def build_setup(self, ctx: CommandContext) -> list[dict[str, Any]]: + """Resolve setup commands from the callable, or return empty list.""" + if self.setup_commands is None: + return [] + return self.setup_commands(ctx) + def build_cleanup(self, ctx: CommandContext) -> list[dict[str, Any]]: """Resolve cleanup commands from the callable, or return empty list.""" if self.cleanup is None: From cbee6560f8c080fbc8177286ad00a2b60ec6b99b Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Fri, 12 Jun 2026 15:12:31 -0700 Subject: [PATCH 05/27] merge AdminTestCase to CommandTestCase Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_behavior.py | 16 ++++---- .../test_setQuerySettings_query_shapes.py | 26 ++++++------- .../test_setQuerySettings_settings.py | 22 +++++------ .../test_setQuerySettings_type_errors.py | 34 ++++++++--------- ...test_setQuerySettings_validation_errors.py | 38 +++++++++---------- .../tests/core/utils/command_test_case.py | 34 +++++------------ 6 files changed, 78 insertions(+), 92 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py index 9f7bbb59a..2063fe191 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py @@ -11,8 +11,8 @@ from pymongo.collection import Collection from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( - AdminCommandTestCase, CommandContext, + CommandTestCase, ) from documentdb_tests.framework.assertions import assertResult, assertSuccessPartial from documentdb_tests.framework.error_codes import QUERYSETTINGS_QUERY_REJECTED_ERROR @@ -54,11 +54,11 @@ def _find_query(ctx: CommandContext, field: str): return {"find": ctx.collection, "filter": {field: 1}, "$db": ctx.database} -# -- Response Structure tests (single-step, fits AdminCommandTestCase) -------- +# -- Response Structure tests (single-step, fits CommandTestCase) -------- # Property [Response Structure]: setQuerySettings response includes hash, query, and settings. -SET_QUERY_SETTINGS_RESPONSE_TESTS: list[AdminCommandTestCase] = [ - AdminCommandTestCase( +SET_QUERY_SETTINGS_RESPONSE_TESTS: list[CommandTestCase] = [ + CommandTestCase( "response_contains_hash", command=lambda ctx: { "setQuerySettings": _find_query(ctx, "b1"), @@ -68,7 +68,7 @@ def _find_query(ctx: CommandContext, field: str): cleanup=_cleanup_query(lambda ctx: _find_query(ctx, "b1")), msg="response should contain queryShapeHash", ), - AdminCommandTestCase( + CommandTestCase( "response_contains_representative_query", command=lambda ctx: { "setQuerySettings": _find_query(ctx, "b2"), @@ -78,7 +78,7 @@ def _find_query(ctx: CommandContext, field: str): cleanup=_cleanup_query(lambda ctx: _find_query(ctx, "b2")), msg="response should contain representativeQuery", ), - AdminCommandTestCase( + CommandTestCase( "response_settings_echo", command=lambda ctx: { "setQuerySettings": _find_query(ctx, "b3"), @@ -117,8 +117,8 @@ def test_setQuerySettings_response(collection, test): # -- removeQuerySettings tests (multi-step: setup creates setting, command removes it) --- # Property [removeQuerySettings]: settings can be removed by query or hash. -SET_QUERY_SETTINGS_REMOVE_TESTS: list[AdminCommandTestCase] = [ - AdminCommandTestCase( +SET_QUERY_SETTINGS_REMOVE_TESTS: list[CommandTestCase] = [ + CommandTestCase( "removeQuerySettings_by_query", setup_commands=lambda ctx: _setup_setting(ctx, _find_query(ctx, "b5")), command=lambda ctx: {"removeQuerySettings": _find_query(ctx, "b5")}, diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py index 7897d85b0..4eaa54dce 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py @@ -10,8 +10,8 @@ import pytest from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( - AdminCommandTestCase, CommandContext, + CommandTestCase, ) from documentdb_tests.framework.assertions import assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command @@ -44,8 +44,8 @@ def _cleanup(query: dict): def _find_case(tid, query_fn, msg): - """Build an AdminCommandTestCase for a find query shape.""" - return AdminCommandTestCase( + """Build an CommandTestCase for a find query shape.""" + return CommandTestCase( tid, command=lambda ctx, qf=query_fn: { "setQuerySettings": qf(ctx), @@ -62,7 +62,7 @@ def _find_case(tid, query_fn, msg): # Property [Distinct Shape Variations]: setQuerySettings accepts distinct shapes with query combos. # Property [Aggregate Shape Variations]: setQuerySettings accepts aggregate pipeline shapes. # Property [$db Field Variations]: setQuerySettings accepts non-existent and special-char db names. -SET_QUERY_SETTINGS_QUERY_SHAPE_TESTS: list[AdminCommandTestCase] = [ +SET_QUERY_SETTINGS_QUERY_SHAPE_TESTS: list[CommandTestCase] = [ # -- Command shape acceptance -- _find_case( "find_shape", @@ -74,7 +74,7 @@ def _find_case(tid, query_fn, msg): }, msg="should accept valid find shape", ), - AdminCommandTestCase( + CommandTestCase( "distinct_shape", command=lambda ctx: { "setQuerySettings": { @@ -98,7 +98,7 @@ def _find_case(tid, query_fn, msg): ], msg="should accept valid distinct shape", ), - AdminCommandTestCase( + CommandTestCase( "aggregate_shape", command=lambda ctx: { "setQuerySettings": { @@ -188,7 +188,7 @@ def _find_case(tid, query_fn, msg): msg="should accept find with limit", ), # -- Distinct shape variations -- - AdminCommandTestCase( + CommandTestCase( "distinct_key_only", command=lambda ctx: { "setQuerySettings": { @@ -210,7 +210,7 @@ def _find_case(tid, query_fn, msg): ], msg="should accept distinct key only", ), - AdminCommandTestCase( + CommandTestCase( "distinct_complex_query", command=lambda ctx: { "setQuerySettings": { @@ -235,7 +235,7 @@ def _find_case(tid, query_fn, msg): msg="should accept distinct complex query", ), # -- Aggregate shape variations -- - AdminCommandTestCase( + CommandTestCase( "aggregate_match_only", command=lambda ctx: { "setQuerySettings": { @@ -257,7 +257,7 @@ def _find_case(tid, query_fn, msg): ], msg="should accept aggregate $match only", ), - AdminCommandTestCase( + CommandTestCase( "aggregate_match_group", command=lambda ctx: { "setQuerySettings": { @@ -285,7 +285,7 @@ def _find_case(tid, query_fn, msg): ], msg="should accept aggregate $match+$group", ), - AdminCommandTestCase( + CommandTestCase( "aggregate_match_sort_limit", command=lambda ctx: { "setQuerySettings": { @@ -308,7 +308,7 @@ def _find_case(tid, query_fn, msg): msg="should accept aggregate $match+$sort+$limit", ), # -- $db field variations -- - AdminCommandTestCase( + CommandTestCase( "db_nonexistent", command=lambda ctx: { "setQuerySettings": { @@ -330,7 +330,7 @@ def _find_case(tid, query_fn, msg): ], msg="should accept non-existent $db", ), - AdminCommandTestCase( + CommandTestCase( "db_special_characters", command=lambda ctx: { "setQuerySettings": { diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py index c0f41a7b4..c6f361e27 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py @@ -10,8 +10,8 @@ import pytest from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( - AdminCommandTestCase, CommandContext, + CommandTestCase, ) from documentdb_tests.framework.assertions import assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command @@ -37,8 +37,8 @@ def _index_hints(ctx: CommandContext, allowed=None): # Property [queryFramework Acceptance]: setQuerySettings accepts classic and sbe frameworks. # Property [comment Acceptance]: setQuerySettings accepts the comment field. # Property [Combined Settings]: setQuerySettings accepts all settings fields together. -SET_QUERY_SETTINGS_SETTINGS_TESTS: list[AdminCommandTestCase] = [ - AdminCommandTestCase( +SET_QUERY_SETTINGS_SETTINGS_TESTS: list[CommandTestCase] = [ + CommandTestCase( "indexHints_single_index", command=lambda ctx: { "setQuerySettings": { @@ -60,7 +60,7 @@ def _index_hints(ctx: CommandContext, allowed=None): ], msg="should accept indexHints with single index", ), - AdminCommandTestCase( + CommandTestCase( "indexHints_multiple_indexes", command=lambda ctx: { "setQuerySettings": { @@ -82,7 +82,7 @@ def _index_hints(ctx: CommandContext, allowed=None): ], msg="should accept multiple indexes", ), - AdminCommandTestCase( + CommandTestCase( "indexHints_key_pattern", command=lambda ctx: { "setQuerySettings": { @@ -104,7 +104,7 @@ def _index_hints(ctx: CommandContext, allowed=None): ], msg="should accept indexHints with key pattern", ), - AdminCommandTestCase( + CommandTestCase( "reject_true", command=lambda ctx: { "setQuerySettings": { @@ -126,7 +126,7 @@ def _index_hints(ctx: CommandContext, allowed=None): ], msg="should accept settings with reject: true", ), - AdminCommandTestCase( + CommandTestCase( "reject_with_indexHints", command=lambda ctx: { "setQuerySettings": { @@ -148,7 +148,7 @@ def _index_hints(ctx: CommandContext, allowed=None): ], msg="should accept reject with indexHints", ), - AdminCommandTestCase( + CommandTestCase( "queryFramework_classic", command=lambda ctx: { "setQuerySettings": { @@ -170,7 +170,7 @@ def _index_hints(ctx: CommandContext, allowed=None): ], msg="should accept queryFramework: classic", ), - AdminCommandTestCase( + CommandTestCase( "queryFramework_sbe", command=lambda ctx: { "setQuerySettings": { @@ -192,7 +192,7 @@ def _index_hints(ctx: CommandContext, allowed=None): ], msg="should accept queryFramework: sbe", ), - AdminCommandTestCase( + CommandTestCase( "with_comment_string", command=lambda ctx: { "setQuerySettings": { @@ -215,7 +215,7 @@ def _index_hints(ctx: CommandContext, allowed=None): ], msg="should accept command with comment string", ), - AdminCommandTestCase( + CommandTestCase( "all_settings_combined", command=lambda ctx: { "setQuerySettings": { diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py index a45870db4..6b5ef24b5 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py @@ -13,8 +13,8 @@ from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( - AdminCommandTestCase, CommandContext, + CommandTestCase, ) from documentdb_tests.framework.assertions import assertResult from documentdb_tests.framework.error_codes import ( @@ -52,8 +52,8 @@ def _default_query(ctx: CommandContext) -> dict: # Property [Primary Argument Type Rejection]: the setQuerySettings field must # be a document (query shape) or string (hash). All other BSON types are # rejected with TYPE_MISMATCH_ERROR. -SET_QUERY_SETTINGS_PRIMARY_ARG_TYPE_TESTS: list[AdminCommandTestCase] = [ - AdminCommandTestCase( +SET_QUERY_SETTINGS_PRIMARY_ARG_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( f"primary_arg_{tid}", command=lambda ctx, v=value: { "setQuerySettings": v, @@ -84,8 +84,8 @@ def _default_query(ctx: CommandContext) -> dict: # Property [queryFramework Type Rejection]: the queryFramework field must be a # string. Non-string BSON types are rejected with TYPE_MISMATCH_ERROR. -SET_QUERY_SETTINGS_QUERY_FRAMEWORK_TYPE_TESTS: list[AdminCommandTestCase] = [ - AdminCommandTestCase( +SET_QUERY_SETTINGS_QUERY_FRAMEWORK_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( f"query_framework_{tid}", command=lambda ctx, v=value: { "setQuerySettings": _default_query(ctx), @@ -116,8 +116,8 @@ def _default_query(ctx: CommandContext) -> dict: # Property [reject Type Rejection]: the reject field must be a boolean. # Non-boolean BSON types are rejected with TYPE_MISMATCH_ERROR. -SET_QUERY_SETTINGS_REJECT_TYPE_TESTS: list[AdminCommandTestCase] = [ - AdminCommandTestCase( +SET_QUERY_SETTINGS_REJECT_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( f"reject_{tid}", command=lambda ctx, v=value: { "setQuerySettings": _default_query(ctx), @@ -147,8 +147,8 @@ def _default_query(ctx: CommandContext) -> dict: ] # Property [indexHints.ns.db Type Rejection]: the ns.db field must be a string. -SET_QUERY_SETTINGS_NS_DB_TYPE_TESTS: list[AdminCommandTestCase] = [ - AdminCommandTestCase( +SET_QUERY_SETTINGS_NS_DB_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( f"ns_db_{tid}", command=lambda ctx, v=value: { "setQuerySettings": _default_query(ctx), @@ -173,8 +173,8 @@ def _default_query(ctx: CommandContext) -> dict: ] # Property [indexHints.ns.coll Type Rejection]: the ns.coll field must be a string. -SET_QUERY_SETTINGS_NS_COLL_TYPE_TESTS: list[AdminCommandTestCase] = [ - AdminCommandTestCase( +SET_QUERY_SETTINGS_NS_COLL_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( f"ns_coll_{tid}", command=lambda ctx, v=value: { "setQuerySettings": _default_query(ctx), @@ -197,8 +197,8 @@ def _default_query(ctx: CommandContext) -> dict: ] # Property [indexHints.allowedIndexes Type Rejection]: allowedIndexes must be an array. -SET_QUERY_SETTINGS_ALLOWED_INDEXES_TYPE_TESTS: list[AdminCommandTestCase] = [ - AdminCommandTestCase( +SET_QUERY_SETTINGS_ALLOWED_INDEXES_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( f"allowed_indexes_{tid}", command=lambda ctx, v=value: { "setQuerySettings": _default_query(ctx), @@ -221,8 +221,8 @@ def _default_query(ctx: CommandContext) -> dict: ] # Property [allowedIndexes null]: null allowedIndexes treated as missing required field. -SET_QUERY_SETTINGS_ALLOWED_INDEXES_EDGE_TESTS: list[AdminCommandTestCase] = [ - AdminCommandTestCase( +SET_QUERY_SETTINGS_ALLOWED_INDEXES_EDGE_TESTS: list[CommandTestCase] = [ + CommandTestCase( "allowed_indexes_null_missing", command=lambda ctx: { "setQuerySettings": _default_query(ctx), @@ -238,7 +238,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=MISSING_FIELD_ERROR, msg="setQuerySettings should reject null allowedIndexes as missing field", ), - AdminCommandTestCase( + CommandTestCase( "allowed_indexes_non_string_element", command=lambda ctx: { "setQuerySettings": _default_query(ctx), @@ -256,7 +256,7 @@ def _default_query(ctx: CommandContext) -> dict: ), ] -SET_QUERY_SETTINGS_TYPE_ERROR_TESTS: list[AdminCommandTestCase] = ( +SET_QUERY_SETTINGS_TYPE_ERROR_TESTS: list[CommandTestCase] = ( SET_QUERY_SETTINGS_PRIMARY_ARG_TYPE_TESTS + SET_QUERY_SETTINGS_QUERY_FRAMEWORK_TYPE_TESTS + SET_QUERY_SETTINGS_REJECT_TYPE_TESTS diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py index 04cb4c811..1f8728e09 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py @@ -10,12 +10,13 @@ import pytest from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( - AdminCommandTestCase, 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_EMPTY_SETTINGS_ERROR, @@ -25,7 +26,6 @@ QUERYSETTINGS_NS_DB_MISSING_ERROR, QUERYSETTINGS_REJECT_ONLY_ERROR, QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, - INVALID_LENGTH_ERROR, UNRECOGNIZED_COMMAND_FIELD_ERROR, ) from documentdb_tests.framework.executor import execute_admin_command @@ -63,8 +63,8 @@ def _default_query(ctx: CommandContext) -> dict: # Property [Unrecognized Fields]: rejects unknown top-level command fields. # Property [Database Restrictions]: rejects query shapes targeting internal databases. # Property [indexHints Value Validation]: rejects empty allowedIndexes and IDHACK queries. -SET_QUERY_SETTINGS_VALIDATION_ERROR_TESTS: list[AdminCommandTestCase] = [ - AdminCommandTestCase( +SET_QUERY_SETTINGS_VALIDATION_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( "query_shape_missing_db", command=lambda ctx: { "setQuerySettings": { @@ -76,7 +76,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=MISSING_FIELD_ERROR, msg="setQuerySettings should reject query shape missing $db field", ), - AdminCommandTestCase( + CommandTestCase( "query_shape_empty_db", command=lambda ctx: { "setQuerySettings": { @@ -89,7 +89,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=INVALID_NAMESPACE_ERROR, msg="setQuerySettings should reject query shape with empty $db", ), - AdminCommandTestCase( + CommandTestCase( "query_shape_unknown_command", command=lambda ctx: { "setQuerySettings": { @@ -102,7 +102,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, msg="setQuerySettings should reject unknown command type in query shape", ), - AdminCommandTestCase( + CommandTestCase( "empty_hash_string", command=lambda ctx: { "setQuerySettings": "", @@ -111,7 +111,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=INVALID_LENGTH_ERROR, msg="setQuerySettings should reject empty hash string", ), - AdminCommandTestCase( + CommandTestCase( "indexHints_missing_ns", command=lambda ctx: { "setQuerySettings": _default_query(ctx), @@ -126,7 +126,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=MISSING_FIELD_ERROR, msg="setQuerySettings should reject indexHints missing ns field", ), - AdminCommandTestCase( + CommandTestCase( "indexHints_ns_missing_db", command=lambda ctx: { "setQuerySettings": _default_query(ctx), @@ -142,7 +142,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=QUERYSETTINGS_NS_DB_MISSING_ERROR, msg="setQuerySettings should reject indexHints.ns missing db field", ), - AdminCommandTestCase( + CommandTestCase( "indexHints_ns_missing_coll", command=lambda ctx: { "setQuerySettings": _default_query(ctx), @@ -158,7 +158,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=QUERYSETTINGS_NS_COLL_MISSING_ERROR, msg="setQuerySettings should reject indexHints.ns missing coll field", ), - AdminCommandTestCase( + CommandTestCase( "invalid_query_framework_value", command=lambda ctx: { "setQuerySettings": _default_query(ctx), @@ -167,7 +167,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=BAD_VALUE_ERROR, msg="setQuerySettings should reject invalid queryFramework string", ), - AdminCommandTestCase( + CommandTestCase( "reject_false_only", command=lambda ctx: { "setQuerySettings": _default_query(ctx), @@ -176,7 +176,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=QUERYSETTINGS_REJECT_ONLY_ERROR, msg="setQuerySettings should reject settings with only reject: false", ), - AdminCommandTestCase( + CommandTestCase( "missing_settings", command=lambda ctx: { "setQuerySettings": _default_query(ctx), @@ -184,7 +184,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=MISSING_FIELD_ERROR, msg="setQuerySettings should reject missing settings field", ), - AdminCommandTestCase( + CommandTestCase( "empty_settings", command=lambda ctx: { "setQuerySettings": _default_query(ctx), @@ -193,7 +193,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=QUERYSETTINGS_EMPTY_SETTINGS_ERROR, msg="setQuerySettings should reject empty settings document", ), - AdminCommandTestCase( + CommandTestCase( "unrecognized_top_level_field", command=lambda ctx: { "setQuerySettings": _default_query(ctx), @@ -203,7 +203,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, msg="setQuerySettings should reject unrecognized top-level field", ), - AdminCommandTestCase( + CommandTestCase( "system_collection", command=lambda ctx: { "setQuerySettings": { @@ -223,7 +223,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=QUERYSETTINGS_INTERNAL_DB_ERROR, msg="setQuerySettings should reject query shapes on internal databases", ), - AdminCommandTestCase( + CommandTestCase( "local_database", command=lambda ctx: { "setQuerySettings": { @@ -243,7 +243,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=QUERYSETTINGS_INTERNAL_DB_ERROR, msg="setQuerySettings should reject query shapes on local database", ), - AdminCommandTestCase( + CommandTestCase( "indexHints_empty_allowed_rejected", command=lambda ctx: { "setQuerySettings": { @@ -263,7 +263,7 @@ def _default_query(ctx: CommandContext) -> dict: error_code=QUERYSETTINGS_REJECT_ONLY_ERROR, msg="setQuerySettings should reject indexHints with empty allowedIndexes", ), - AdminCommandTestCase( + CommandTestCase( "idhack_query_rejected", command=lambda ctx: { "setQuerySettings": { diff --git a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py index e31147db4..e5fc9fc12 100644 --- a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py +++ b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py @@ -69,6 +69,14 @@ class CommandTestCase(BaseTestCase): for error cases. ignore_order_in: Optional names of result fields whose array contents should be compared without regard to element order. + setup_commands: Optional callable ``(CommandContext) -> list[dict]`` + returning commands to execute **before** the main command. + Use for prerequisite operations such as creating a query + setting before testing removal. + cleanup: Optional callable ``(CommandContext) -> list[dict]`` + returning commands to run after the test. Each dict is + passed to the executor inside a try/except so cleanup + failures are silently ignored. """ target_collection: TargetCollection = field(default_factory=TargetCollection) @@ -78,6 +86,8 @@ class CommandTestCase(BaseTestCase): command: dict[str, Any] | Callable[..., dict[str, Any]] | None = None expected: dict[str, Any] | list[dict[str, Any]] | Callable[..., dict[str, Any]] | None = None ignore_order_in: list[str] | None = None + setup_commands: Callable[[CommandContext], list[dict[str, Any]]] | None = None + cleanup: Callable[[CommandContext], list[dict[str, Any]]] | None = None def prepare(self, db: Database, collection: Collection) -> Collection: """Resolve the target collection and apply indexes/docs. @@ -119,30 +129,6 @@ def build_expected(self, ctx: CommandContext) -> dict[str, Any] | list[dict[str, return self.expected return self.expected(ctx) - -@dataclass(frozen=True) -class AdminCommandTestCase(CommandTestCase): - """Test case for admin-level commands (e.g. setQuerySettings). - - Admin commands run against the ``admin`` database via - ``execute_admin_command`` rather than against a specific collection's - database. They often need post-test cleanup (e.g. removing query - settings that were created). - - Attributes: - setup_commands: Optional callable ``(CommandContext) -> list[dict]`` - returning admin commands to execute **before** the main - command. Use for prerequisite operations such as creating - a query setting before testing removal. - cleanup: Optional callable ``(CommandContext) -> list[dict]`` - returning admin commands to run after the test. Each dict - is passed to ``execute_admin_command`` inside a try/except - so cleanup failures are silently ignored. - """ - - setup_commands: Callable[[CommandContext], list[dict[str, Any]]] | None = None - cleanup: Callable[[CommandContext], list[dict[str, Any]]] | None = None - def build_setup(self, ctx: CommandContext) -> list[dict[str, Any]]: """Resolve setup commands from the callable, or return empty list.""" if self.setup_commands is None: From af42a655953399f937cae7f3a6945d3f58b7ccce Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 14:29:08 -0700 Subject: [PATCH 06/27] replace helper functions to inline Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_behavior.py | 169 +++++--- .../test_setQuerySettings_query_shapes.py | 386 +++++++++++++----- .../test_setQuerySettings_settings.py | 127 ++++-- .../test_setQuerySettings_type_errors.py | 95 +++-- ...test_setQuerySettings_validation_errors.py | 136 ++++-- 5 files changed, 657 insertions(+), 256 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py index 2063fe191..56b0fa354 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py @@ -21,71 +21,103 @@ from .utils.setQuerySettings_common import cleanup_query_settings, get_query_settings -# -- helpers ------------------------------------------------------------------ - - -def _index_hints(ctx: CommandContext): - """Build a standard indexHints array for the fixture collection.""" - return [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ] - - -def _settings(ctx: CommandContext): - """Build a standard settings block with indexHints.""" - return {"indexHints": _index_hints(ctx)} - - -def _setup_setting(ctx: CommandContext, query: dict, settings: dict | None = None): - """Return a setup command list that creates a query setting.""" - return [{"setQuerySettings": query, "settings": settings or _settings(ctx)}] - - -def _cleanup_query(query_fn): - """Return a cleanup callable that removes the query shape built by query_fn.""" - return lambda ctx: [{"removeQuerySettings": query_fn(ctx)}] - - -def _find_query(ctx: CommandContext, field: str): - """Build a find query shape for the given field.""" - return {"find": ctx.collection, "filter": {field: 1}, "$db": ctx.database} - - -# -- Response Structure tests (single-step, fits CommandTestCase) -------- - # Property [Response Structure]: setQuerySettings response includes hash, query, and settings. SET_QUERY_SETTINGS_RESPONSE_TESTS: list[CommandTestCase] = [ CommandTestCase( "response_contains_hash", command=lambda ctx: { - "setQuerySettings": _find_query(ctx, "b1"), - "settings": _settings(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"b1": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, - cleanup=_cleanup_query(lambda ctx: _find_query(ctx, "b1")), + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b1": 1}, + "$db": ctx.database, + } + } + ], msg="response should contain queryShapeHash", ), CommandTestCase( "response_contains_representative_query", command=lambda ctx: { - "setQuerySettings": _find_query(ctx, "b2"), - "settings": _settings(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"b2": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, - cleanup=_cleanup_query(lambda ctx: _find_query(ctx, "b2")), + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b2": 1}, + "$db": ctx.database, + } + } + ], msg="response should contain representativeQuery", ), CommandTestCase( "response_settings_echo", command=lambda ctx: { - "setQuerySettings": _find_query(ctx, "b3"), - "settings": _settings(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"b3": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + expected=lambda ctx: { + "ok": 1.0, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, - expected=lambda ctx: {"ok": 1.0, "settings": _settings(ctx)}, - cleanup=_cleanup_query(lambda ctx: _find_query(ctx, "b3")), + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b3": 1}, + "$db": ctx.database, + } + } + ], msg="response should echo applied settings", ), ] @@ -114,16 +146,44 @@ def test_setQuerySettings_response(collection, test): pass -# -- removeQuerySettings tests (multi-step: setup creates setting, command removes it) --- - # Property [removeQuerySettings]: settings can be removed by query or hash. SET_QUERY_SETTINGS_REMOVE_TESTS: list[CommandTestCase] = [ CommandTestCase( "removeQuerySettings_by_query", - setup_commands=lambda ctx: _setup_setting(ctx, _find_query(ctx, "b5")), - command=lambda ctx: {"removeQuerySettings": _find_query(ctx, "b5")}, + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"b5": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + } + ], + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b5": 1}, + "$db": ctx.database, + } + }, expected={"ok": 1.0}, - cleanup=_cleanup_query(lambda ctx: _find_query(ctx, "b5")), + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b5": 1}, + "$db": ctx.database, + } + } + ], msg="removeQuerySettings by query should succeed", ), ] @@ -148,9 +208,6 @@ def test_setQuerySettings_remove(collection, test): pass -# -- Multi-step behavior tests (kept as individual functions) ----------------- - - # Property [removeQuerySettings by hash]: requires capturing hash from setup result. @pytest.mark.admin @pytest.mark.replica_set @@ -162,7 +219,6 @@ def test_setQuerySettings_removeQuerySettings_by_hash(collection: Collection): "$db": collection.database.name, } try: - # Setup: create a query setting and capture hash (no assertion — setup only) setup_result = execute_admin_command( collection, { @@ -199,7 +255,6 @@ def test_setQuerySettings_querySettings_stage_retrieval(collection: Collection): "$db": collection.database.name, } try: - # Setup: create a query setting (no assertion — setup only) setup_result = execute_admin_command( collection, { @@ -238,7 +293,6 @@ def test_setQuerySettings_reject_true_blocks_query(collection: Collection): "$db": collection.database.name, } try: - # Setup: create a reject setting (no assertion — setup only) execute_admin_command( collection, { @@ -247,7 +301,6 @@ def test_setQuerySettings_reject_true_blocks_query(collection: Collection): }, ) - # Execute the matching find query on the collection database result = execute_command( collection, { @@ -274,7 +327,6 @@ def test_setQuerySettings_querySettings_stage_shows_settings(collection: Collect "$db": collection.database.name, } try: - # Setup: create a query setting (no assertion — setup only) setup_result = execute_admin_command( collection, { @@ -322,7 +374,6 @@ def test_setQuerySettings_querySettings_stage_shows_representative_query(collect "$db": collection.database.name, } try: - # Setup: create a query setting (no assertion — setup only) setup_result = execute_admin_command( collection, { diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py index 4eaa54dce..1e6eceee6 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py @@ -17,46 +17,6 @@ from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params -# -- helpers ------------------------------------------------------------------ - - -def _index_hints(ctx: CommandContext, db=None, coll=None): - """Build a standard indexHints array, optionally overriding db/coll.""" - return [ - { - "ns": {"db": db or ctx.database, "coll": coll or ctx.collection}, - "allowedIndexes": ["_id_"], - } - ] - - -def _settings(ctx: CommandContext, db=None, coll=None): - """Build a standard settings block with indexHints.""" - return {"indexHints": _index_hints(ctx, db=db, coll=coll)} - - -def _cleanup(query: dict): - """Return a cleanup callable that removes the given query shape.""" - return lambda ctx: [{"removeQuerySettings": query}] - - -# -- test case helpers -------------------------------------------------------- - - -def _find_case(tid, query_fn, msg): - """Build an CommandTestCase for a find query shape.""" - return CommandTestCase( - tid, - command=lambda ctx, qf=query_fn: { - "setQuerySettings": qf(ctx), - "settings": _settings(ctx), - }, - expected={"ok": 1.0}, - cleanup=lambda ctx, qf=query_fn: [{"removeQuerySettings": qf(ctx)}], - msg=msg, - ) - - # Property [Command Shape Acceptance]: accepts find, distinct, and aggregate shapes. # Property [Find Shape Variations]: setQuerySettings accepts find shapes with various field combos. # Property [Distinct Shape Variations]: setQuerySettings accepts distinct shapes with query combos. @@ -64,14 +24,35 @@ def _find_case(tid, query_fn, msg): # Property [$db Field Variations]: setQuerySettings accepts non-existent and special-char db names. SET_QUERY_SETTINGS_QUERY_SHAPE_TESTS: list[CommandTestCase] = [ # -- Command shape acceptance -- - _find_case( + CommandTestCase( "find_shape", - lambda ctx: { - "find": ctx.collection, - "filter": {"x": 1}, - "sort": {"x": 1}, - "$db": ctx.database, + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "sort": {"x": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "sort": {"x": 1}, + "$db": ctx.database, + } + } + ], msg="should accept valid find shape", ), CommandTestCase( @@ -83,7 +64,14 @@ def _find_case(tid, query_fn, msg): "query": {"x": {"$gt": 0}}, "$db": ctx.database, }, - "settings": _settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -106,7 +94,14 @@ def _find_case(tid, query_fn, msg): "pipeline": [{"$match": {"x": 1}}], "$db": ctx.database, }, - "settings": _settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -121,70 +116,221 @@ def _find_case(tid, query_fn, msg): msg="should accept valid aggregate shape", ), # -- Find shape variations -- - _find_case( + CommandTestCase( "find_filter_only", - lambda ctx: {"find": ctx.collection, "filter": {"a": 1}, "$db": ctx.database}, + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a": 1}, + "$db": ctx.database, + } + } + ], msg="should accept find with filter only", ), - _find_case( + CommandTestCase( "find_filter_sort", - lambda ctx: { - "find": ctx.collection, - "filter": {"b": 1}, - "sort": {"b": 1}, - "$db": ctx.database, + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"b": 1}, + "sort": {"b": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b": 1}, + "sort": {"b": 1}, + "$db": ctx.database, + } + } + ], msg="should accept find with filter+sort", ), - _find_case( + CommandTestCase( "find_filter_projection", - lambda ctx: { - "find": ctx.collection, - "filter": {"c": 1}, - "projection": {"c": 1}, - "$db": ctx.database, + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"c": 1}, + "projection": {"c": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"c": 1}, + "projection": {"c": 1}, + "$db": ctx.database, + } + } + ], msg="should accept find with filter+projection", ), - _find_case( + CommandTestCase( "find_filter_sort_projection", - lambda ctx: { - "find": ctx.collection, - "filter": {"d": 1}, - "sort": {"d": 1}, - "projection": {"d": 1}, - "$db": ctx.database, + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"d": 1}, + "sort": {"d": 1}, + "projection": {"d": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"d": 1}, + "sort": {"d": 1}, + "projection": {"d": 1}, + "$db": ctx.database, + } + } + ], msg="should accept find with all fields", ), - _find_case( + CommandTestCase( "find_with_collation", - lambda ctx: { - "find": ctx.collection, - "filter": {"e": "abc"}, - "collation": {"locale": "en", "strength": 2}, - "$db": ctx.database, + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"e": "abc"}, + "collation": {"locale": "en", "strength": 2}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"e": "abc"}, + "collation": {"locale": "en", "strength": 2}, + "$db": ctx.database, + } + } + ], msg="should accept find with collation", ), - _find_case( + CommandTestCase( "find_with_let", - lambda ctx: { - "find": ctx.collection, - "filter": {"$expr": {"$eq": ["$f", "$$target"]}}, - "let": {"target": 1}, - "$db": ctx.database, + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"$expr": {"$eq": ["$f", "$$target"]}}, + "let": {"target": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"$expr": {"$eq": ["$f", "$$target"]}}, + "let": {"target": 1}, + "$db": ctx.database, + } + } + ], msg="should accept find with let", ), - _find_case( + CommandTestCase( "find_with_limit", - lambda ctx: { - "find": ctx.collection, - "filter": {"g": 1}, - "limit": 10, - "$db": ctx.database, + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"g": 1}, + "limit": 10, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"g": 1}, + "limit": 10, + "$db": ctx.database, + } + } + ], msg="should accept find with limit", ), # -- Distinct shape variations -- @@ -196,7 +342,14 @@ def _find_case(tid, query_fn, msg): "key": "j", "$db": ctx.database, }, - "settings": _settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -219,7 +372,14 @@ def _find_case(tid, query_fn, msg): "query": {"$and": [{"k": {"$gt": 0}}, {"k": {"$lt": 100}}]}, "$db": ctx.database, }, - "settings": _settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -243,7 +403,14 @@ def _find_case(tid, query_fn, msg): "pipeline": [{"$match": {"l": 1}}], "$db": ctx.database, }, - "settings": _settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -268,7 +435,14 @@ def _find_case(tid, query_fn, msg): ], "$db": ctx.database, }, - "settings": _settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -293,7 +467,14 @@ def _find_case(tid, query_fn, msg): "pipeline": [{"$match": {"n": 1}}, {"$sort": {"n": 1}}, {"$limit": 5}], "$db": ctx.database, }, - "settings": _settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -316,7 +497,17 @@ def _find_case(tid, query_fn, msg): "filter": {"o": 1}, "$db": "nonexistent_db_for_query_settings_test", }, - "settings": _settings(ctx, db="nonexistent_db_for_query_settings_test"), + "settings": { + "indexHints": [ + { + "ns": { + "db": "nonexistent_db_for_query_settings_test", + "coll": ctx.collection, + }, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -338,7 +529,14 @@ def _find_case(tid, query_fn, msg): "filter": {"p": 1}, "$db": "test-special-db", }, - "settings": _settings(ctx, db="test-special-db"), + "settings": { + "indexHints": [ + { + "ns": {"db": "test-special-db", "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py index c6f361e27..689718a21 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py @@ -19,19 +19,6 @@ from .utils.setQuerySettings_common import cleanup_query_settings -# -- helpers ------------------------------------------------------------------ - - -def _index_hints(ctx: CommandContext, allowed=None): - """Build a standard indexHints array for the fixture collection.""" - return [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": allowed or ["_id_"], - } - ] - - # Property [indexHints Acceptance]: setQuerySettings accepts valid indexHints configurations. # Property [reject Acceptance]: setQuerySettings accepts reject: true alone or with indexHints. # Property [queryFramework Acceptance]: setQuerySettings accepts classic and sbe frameworks. @@ -46,7 +33,14 @@ def _index_hints(ctx: CommandContext, allowed=None): "filter": {"a1": 1}, "$db": ctx.database, }, - "settings": {"indexHints": _index_hints(ctx)}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -68,7 +62,14 @@ def _index_hints(ctx: CommandContext, allowed=None): "filter": {"a2": 1}, "$db": ctx.database, }, - "settings": {"indexHints": _index_hints(ctx, ["_id_", {"a2": 1}])}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_", {"a2": 1}], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -90,7 +91,14 @@ def _index_hints(ctx: CommandContext, allowed=None): "filter": {"a3": 1}, "$db": ctx.database, }, - "settings": {"indexHints": _index_hints(ctx, [{"a3": 1}])}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": [{"a3": 1}], + } + ], + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -134,7 +142,15 @@ def _index_hints(ctx: CommandContext, allowed=None): "filter": {"a6": 1}, "$db": ctx.database, }, - "settings": {"indexHints": _index_hints(ctx), "reject": True}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "reject": True, + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -156,7 +172,15 @@ def _index_hints(ctx: CommandContext, allowed=None): "filter": {"a7": 1}, "$db": ctx.database, }, - "settings": {"indexHints": _index_hints(ctx), "queryFramework": "classic"}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": "classic", + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -178,7 +202,15 @@ def _index_hints(ctx: CommandContext, allowed=None): "filter": {"a8": 1}, "$db": ctx.database, }, - "settings": {"indexHints": _index_hints(ctx), "queryFramework": "sbe"}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": "sbe", + }, }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -200,7 +232,14 @@ def _index_hints(ctx: CommandContext, allowed=None): "filter": {"a9": 1}, "$db": ctx.database, }, - "settings": {"indexHints": _index_hints(ctx)}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, "comment": "test comment for setQuerySettings", }, expected={"ok": 1.0}, @@ -224,7 +263,12 @@ def _index_hints(ctx: CommandContext, allowed=None): "$db": ctx.database, }, "settings": { - "indexHints": _index_hints(ctx), + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], "queryFramework": "classic", "reject": True, }, @@ -261,9 +305,6 @@ def test_setQuerySettings_settings(collection, test): pass -# -- Update Behavior tests (multi-step, kept as individual functions) --------- - - # Property [Update Behavior]: setQuerySettings can update existing settings by query or hash. @pytest.mark.admin @pytest.mark.replica_set @@ -276,12 +317,18 @@ def test_setQuerySettings_update_existing_settings(collection): "$db": ctx.database, } try: - # Setup: create initial settings (no assertion — setup only) execute_admin_command( collection, { "setQuerySettings": query, - "settings": {"indexHints": _index_hints(ctx)}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, ) @@ -289,7 +336,14 @@ def test_setQuerySettings_update_existing_settings(collection): collection, { "setQuerySettings": query, - "settings": {"indexHints": _index_hints(ctx, ["_id_", {"a10": 1}])}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_", {"a10": 1}], + } + ], + }, }, ) assertSuccessPartial(result, {"ok": 1.0}, msg="update setQuerySettings should succeed") @@ -308,12 +362,18 @@ def test_setQuerySettings_update_via_hash(collection): "$db": ctx.database, } try: - # Setup: create initial settings and capture hash (no assertion — setup only) setup_result = execute_admin_command( collection, { "setQuerySettings": query, - "settings": {"indexHints": _index_hints(ctx)}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, ) @@ -322,7 +382,14 @@ def test_setQuerySettings_update_via_hash(collection): collection, { "setQuerySettings": query_hash, - "settings": {"indexHints": _index_hints(ctx, ["_id_", {"a11": 1}])}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_", {"a11": 1}], + } + ], + }, }, ) assertSuccessPartial(result, {"ok": 1.0}, msg="update via hash should succeed") diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py index 6b5ef24b5..2c9d0304d 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py @@ -25,30 +25,6 @@ from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params -# -- helpers ------------------------------------------------------------------ - - -def _default_settings(ctx: CommandContext) -> dict: - """Build the standard indexHints settings block.""" - return { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - } - - -def _default_query(ctx: CommandContext) -> dict: - """Build a minimal valid query shape.""" - return { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - } - - # Property [Primary Argument Type Rejection]: the setQuerySettings field must # be a document (query shape) or string (hash). All other BSON types are # rejected with TYPE_MISMATCH_ERROR. @@ -57,7 +33,14 @@ def _default_query(ctx: CommandContext) -> dict: f"primary_arg_{tid}", command=lambda ctx, v=value: { "setQuerySettings": v, - "settings": _default_settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, error_code=TYPE_MISMATCH_ERROR, msg=f"setQuerySettings should reject {tid} as the primary argument", @@ -88,8 +71,20 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( f"query_framework_{tid}", command=lambda ctx, v=value: { - "setQuerySettings": _default_query(ctx), - "settings": {**_default_settings(ctx), "queryFramework": v}, + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": v, + }, }, error_code=TYPE_MISMATCH_ERROR, msg=f"setQuerySettings should reject {tid} as queryFramework", @@ -120,8 +115,20 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( f"reject_{tid}", command=lambda ctx, v=value: { - "setQuerySettings": _default_query(ctx), - "settings": {**_default_settings(ctx), "reject": v}, + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "reject": v, + }, }, error_code=TYPE_MISMATCH_ERROR, msg=f"setQuerySettings should reject {tid} as reject field", @@ -151,7 +158,11 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( f"ns_db_{tid}", command=lambda ctx, v=value: { - "setQuerySettings": _default_query(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { @@ -177,7 +188,11 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( f"ns_coll_{tid}", command=lambda ctx, v=value: { - "setQuerySettings": _default_query(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { @@ -201,7 +216,11 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( f"allowed_indexes_{tid}", command=lambda ctx, v=value: { - "setQuerySettings": _default_query(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { @@ -225,7 +244,11 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( "allowed_indexes_null_missing", command=lambda ctx: { - "setQuerySettings": _default_query(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { @@ -241,7 +264,11 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( "allowed_indexes_non_string_element", command=lambda ctx: { - "setQuerySettings": _default_query(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py index 1f8728e09..73c423695 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py @@ -31,30 +31,6 @@ from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params -# -- helpers ------------------------------------------------------------------ - - -def _default_settings(ctx: CommandContext) -> dict: - """Build the standard indexHints settings block.""" - return { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - } - - -def _default_query(ctx: CommandContext) -> dict: - """Build a minimal valid query shape.""" - return { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - } - - # Property [Query Shape Validation]: rejects malformed or unknown query shape documents. # Property [Hash String Validation]: rejects invalid hash string formats. # Property [indexHints Structure Validation]: rejects indexHints missing required sub-fields. @@ -71,7 +47,14 @@ def _default_query(ctx: CommandContext) -> dict: "find": ctx.collection, "filter": {"x": 1}, }, - "settings": _default_settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, error_code=MISSING_FIELD_ERROR, msg="setQuerySettings should reject query shape missing $db field", @@ -84,7 +67,14 @@ def _default_query(ctx: CommandContext) -> dict: "filter": {"x": 1}, "$db": "", }, - "settings": _default_settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, error_code=INVALID_NAMESPACE_ERROR, msg="setQuerySettings should reject query shape with empty $db", @@ -97,7 +87,14 @@ def _default_query(ctx: CommandContext) -> dict: "filter": {"x": 1}, "$db": ctx.database, }, - "settings": _default_settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, error_code=QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, msg="setQuerySettings should reject unknown command type in query shape", @@ -106,7 +103,14 @@ def _default_query(ctx: CommandContext) -> dict: "empty_hash_string", command=lambda ctx: { "setQuerySettings": "", - "settings": _default_settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, error_code=INVALID_LENGTH_ERROR, msg="setQuerySettings should reject empty hash string", @@ -114,7 +118,11 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( "indexHints_missing_ns", command=lambda ctx: { - "setQuerySettings": _default_query(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { @@ -129,7 +137,11 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( "indexHints_ns_missing_db", command=lambda ctx: { - "setQuerySettings": _default_query(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { @@ -145,7 +157,11 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( "indexHints_ns_missing_coll", command=lambda ctx: { - "setQuerySettings": _default_query(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { @@ -161,8 +177,20 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( "invalid_query_framework_value", command=lambda ctx: { - "setQuerySettings": _default_query(ctx), - "settings": {**_default_settings(ctx), "queryFramework": "invalidFramework"}, + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": "invalidFramework", + }, }, error_code=BAD_VALUE_ERROR, msg="setQuerySettings should reject invalid queryFramework string", @@ -170,7 +198,11 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( "reject_false_only", command=lambda ctx: { - "setQuerySettings": _default_query(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, "settings": {"reject": False}, }, error_code=QUERYSETTINGS_REJECT_ONLY_ERROR, @@ -179,7 +211,11 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( "missing_settings", command=lambda ctx: { - "setQuerySettings": _default_query(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, }, error_code=MISSING_FIELD_ERROR, msg="setQuerySettings should reject missing settings field", @@ -187,7 +223,11 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( "empty_settings", command=lambda ctx: { - "setQuerySettings": _default_query(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, "settings": {}, }, error_code=QUERYSETTINGS_EMPTY_SETTINGS_ERROR, @@ -196,8 +236,19 @@ def _default_query(ctx: CommandContext) -> dict: CommandTestCase( "unrecognized_top_level_field", command=lambda ctx: { - "setQuerySettings": _default_query(ctx), - "settings": _default_settings(ctx), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, "unknownField": 1, }, error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, @@ -271,7 +322,14 @@ def _default_query(ctx: CommandContext) -> dict: "filter": {"_id": 1}, "$db": ctx.database, }, - "settings": _default_settings(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, error_code=QUERYSETTINGS_IDHACK_QUERY_ERROR, msg="setQuerySettings should reject IDHACK-eligible queries", From 57e5233e1bfe621b5fb95e429ac14b860c7f1218 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 14:50:12 -0700 Subject: [PATCH 07/27] group error cases together Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_behavior.py | 40 +---------------- ...test_setQuerySettings_validation_errors.py | 45 ++++++++++++++++++- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py index 56b0fa354..c5e62c023 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py @@ -14,9 +14,8 @@ CommandContext, CommandTestCase, ) -from documentdb_tests.framework.assertions import assertResult, assertSuccessPartial -from documentdb_tests.framework.error_codes import QUERYSETTINGS_QUERY_REJECTED_ERROR -from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params from .utils.setQuerySettings_common import cleanup_query_settings, get_query_settings @@ -282,41 +281,6 @@ def test_setQuerySettings_querySettings_stage_retrieval(collection: Collection): cleanup_query_settings(collection, [query]) -# Property [Reject Blocks Query]: a rejected query returns an error when executed. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_reject_true_blocks_query(collection: Collection): - """Test that reject: true causes the matching query to be rejected.""" - query = { - "find": collection.name, - "filter": {"b8": 1}, - "$db": collection.database.name, - } - try: - execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": {"reject": True}, - }, - ) - - result = execute_command( - collection, - { - "find": collection.name, - "filter": {"b8": 1}, - }, - ) - assertResult( - result, - error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, - msg="query matching reject: true setting should be rejected", - ) - finally: - cleanup_query_settings(collection, [query]) - - @pytest.mark.admin @pytest.mark.replica_set def test_setQuerySettings_querySettings_stage_shows_settings(collection: Collection): diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py index 73c423695..55e1577bc 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py @@ -2,12 +2,14 @@ Validates that the setQuerySettings command rejects malformed query shapes, invalid hash strings, missing or empty settings, unrecognized fields, invalid -queryFramework values, and system collection restrictions. +queryFramework values, system collection restrictions, and that reject: true +blocks matching queries at execution time. """ from __future__ import annotations import pytest +from pymongo.collection import Collection from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, @@ -24,13 +26,16 @@ QUERYSETTINGS_INTERNAL_DB_ERROR, QUERYSETTINGS_NS_COLL_MISSING_ERROR, QUERYSETTINGS_NS_DB_MISSING_ERROR, + QUERYSETTINGS_QUERY_REJECTED_ERROR, QUERYSETTINGS_REJECT_ONLY_ERROR, QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, UNRECOGNIZED_COMMAND_FIELD_ERROR, ) -from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params +from .utils.setQuerySettings_common import cleanup_query_settings + # Property [Query Shape Validation]: rejects malformed or unknown query shape documents. # Property [Hash String Validation]: rejects invalid hash string formats. # Property [indexHints Structure Validation]: rejects indexHints missing required sub-fields. @@ -39,6 +44,7 @@ # Property [Unrecognized Fields]: rejects unknown top-level command fields. # Property [Database Restrictions]: rejects query shapes targeting internal databases. # Property [indexHints Value Validation]: rejects empty allowedIndexes and IDHACK queries. +# Property [Reject Blocks Query]: a rejected query returns an error when executed. SET_QUERY_SETTINGS_VALIDATION_ERROR_TESTS: list[CommandTestCase] = [ CommandTestCase( "query_shape_missing_db", @@ -349,3 +355,38 @@ def test_setQuerySettings_validation_errors(collection, test): error_code=test.error_code, msg=test.msg, ) + + +# Property [Reject Blocks Query]: a rejected query returns an error when executed. +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_reject_true_blocks_query(collection: Collection): + """Test that reject: true causes the matching query to be rejected.""" + query = { + "find": collection.name, + "filter": {"b8": 1}, + "$db": collection.database.name, + } + try: + execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": {"reject": True}, + }, + ) + + result = execute_command( + collection, + { + "find": collection.name, + "filter": {"b8": 1}, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, + msg="query matching reject: true setting should be rejected", + ) + finally: + cleanup_query_settings(collection, [query]) From c0c7d98b3bba2f276b45a84f182e17868ad5e734 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 15:08:10 -0700 Subject: [PATCH 08/27] add missing tests Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_query_shapes.py | 29 + .../test_setQuerySettings_reject.py | 233 +++++++ .../test_setQuerySettings_settings.py | 250 +++++++- ...test_setQuerySettings_validation_errors.py | 73 +++ .../test_setQuerySettings_verification.py | 595 ++++++++++++++++++ 5 files changed, 1177 insertions(+), 3 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py create mode 100644 documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py index 1e6eceee6..ec9bd0469 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py @@ -488,6 +488,35 @@ ], msg="should accept aggregate $match+$sort+$limit", ), + CommandTestCase( + "aggregate_empty_pipeline", + command=lambda ctx: { + "setQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [], + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [], + "$db": ctx.database, + } + } + ], + msg="should accept aggregate with empty pipeline", + ), # -- $db field variations -- CommandTestCase( "db_nonexistent", diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py new file mode 100644 index 000000000..5b1a34cca --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py @@ -0,0 +1,233 @@ +"""Tests for setQuerySettings reject field behavior. + +Validates that reject: true blocks matching queries for find, distinct, and +aggregate commands, that rejection does not affect unrelated query shapes, +and that reject can be reversed via update or removal. +""" + +from __future__ import annotations + +import pytest +from pymongo.collection import Collection + +from documentdb_tests.framework.assertions import assertResult, assertSuccessPartial +from documentdb_tests.framework.error_codes import QUERYSETTINGS_QUERY_REJECTED_ERROR +from documentdb_tests.framework.executor import execute_admin_command, execute_command + +from .utils.setQuerySettings_common import cleanup_query_settings + +# Property [Reject Blocks Distinct]: reject: true blocks matching distinct queries. +# Property [Reject Blocks Aggregate]: reject: true blocks matching aggregate queries. +# Property [Reject Scope]: reject: true does not affect unrelated query shapes. +# Property [Reject Reversal via Update]: updating reject to false re-enables the query. +# Property [Reject Reversal via Remove]: removing the query setting re-enables the query. +# Property [Reject False Succeeds]: reject: false with indexHints allows the query. + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_reject_blocks_distinct(collection: Collection): + """Test that reject: true blocks a matching distinct query.""" + query = { + "distinct": collection.name, + "key": "x", + "query": {"rej_d1": 1}, + "$db": collection.database.name, + } + try: + execute_admin_command( + collection, + {"setQuerySettings": query, "settings": {"reject": True}}, + ) + result = execute_command( + collection, + { + "distinct": collection.name, + "key": "x", + "query": {"rej_d1": 1}, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, + msg="distinct query matching reject: true should be rejected", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_reject_blocks_aggregate(collection: Collection): + """Test that reject: true blocks a matching aggregate query.""" + query = { + "aggregate": collection.name, + "pipeline": [{"$match": {"rej_a1": 1}}], + "$db": collection.database.name, + } + try: + execute_admin_command( + collection, + {"setQuerySettings": query, "settings": {"reject": True}}, + ) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [{"$match": {"rej_a1": 1}}], + "cursor": {}, + }, + ) + assertResult( + result, + error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, + msg="aggregate query matching reject: true should be rejected", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_reject_does_not_affect_different_shape( + collection: Collection, +): + """Test that reject: true for one shape does not reject a different shape.""" + query = { + "find": collection.name, + "filter": {"rej_s1": 1}, + "$db": collection.database.name, + } + try: + execute_admin_command( + collection, + {"setQuerySettings": query, "settings": {"reject": True}}, + ) + result = execute_command( + collection, + {"find": collection.name, "filter": {"rej_s2": 1}}, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="different query shape should not be rejected", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_reject_reversed_by_update(collection: Collection): + """Test that updating reject from true to false re-enables the query.""" + query = { + "find": collection.name, + "filter": {"rej_u1": 1}, + "$db": collection.database.name, + } + try: + execute_admin_command( + collection, + {"setQuerySettings": query, "settings": {"reject": True}}, + ) + execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "reject": False, + "indexHints": [ + { + "ns": { + "db": collection.database.name, + "coll": collection.name, + }, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + result = execute_command( + collection, + {"find": collection.name, "filter": {"rej_u1": 1}}, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="query should succeed after reject updated to false", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_reject_reversed_by_remove(collection: Collection): + """Test that removeQuerySettings re-enables a previously rejected query.""" + query = { + "find": collection.name, + "filter": {"rej_r1": 1}, + "$db": collection.database.name, + } + try: + execute_admin_command( + collection, + {"setQuerySettings": query, "settings": {"reject": True}}, + ) + execute_admin_command( + collection, + {"removeQuerySettings": query}, + ) + result = execute_command( + collection, + {"find": collection.name, "filter": {"rej_r1": 1}}, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="query should succeed after removeQuerySettings", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_reject_false_with_hints(collection: Collection): + """Test that reject: false with indexHints allows the matching query.""" + query = { + "find": collection.name, + "filter": {"rej_f1": 1}, + "$db": collection.database.name, + } + try: + execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "reject": False, + "indexHints": [ + { + "ns": { + "db": collection.database.name, + "coll": collection.name, + }, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + result = execute_command( + collection, + {"find": collection.name, "filter": {"rej_f1": 1}}, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="query with reject: false should succeed", + ) + finally: + cleanup_query_settings(collection, [query]) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py index 689718a21..536c42e34 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py @@ -22,8 +22,11 @@ # Property [indexHints Acceptance]: setQuerySettings accepts valid indexHints configurations. # Property [reject Acceptance]: setQuerySettings accepts reject: true alone or with indexHints. # Property [queryFramework Acceptance]: setQuerySettings accepts classic and sbe frameworks. -# Property [comment Acceptance]: setQuerySettings accepts the comment field. +# Property [comment Acceptance]: setQuerySettings accepts comment as any BSON type. # Property [Combined Settings]: setQuerySettings accepts all settings fields together. +# Property [$natural Hint]: setQuerySettings accepts $natural in allowedIndexes. +# Property [Multiple indexHints]: setQuerySettings accepts multiple indexHints documents. +# Property [Non-Existent Index]: setQuerySettings accepts non-existent index names. SET_QUERY_SETTINGS_SETTINGS_TESTS: list[CommandTestCase] = [ CommandTestCase( "indexHints_single_index", @@ -239,8 +242,8 @@ "allowedIndexes": ["_id_"], } ], + "comment": "test comment for setQuerySettings", }, - "comment": "test comment for setQuerySettings", }, expected={"ok": 1.0}, cleanup=lambda ctx: [ @@ -252,7 +255,7 @@ } } ], - msg="should accept command with comment string", + msg="should accept settings with comment string", ), CommandTestCase( "all_settings_combined", @@ -285,6 +288,247 @@ ], msg="should accept all settings combined", ), + CommandTestCase( + "indexHints_natural", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a13": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["$natural"], + } + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a13": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept $natural in allowedIndexes", + ), + CommandTestCase( + "indexHints_multiple_ns_documents", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a14": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + }, + { + "ns": {"db": ctx.database, "coll": "other_collection"}, + "allowedIndexes": ["_id_"], + }, + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a14": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept multiple indexHints documents", + ), + CommandTestCase( + "indexHints_nonexistent_index", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a15": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["nonexistent_index"], + } + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a15": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept non-existent index name", + ), + CommandTestCase( + "comment_object", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a16": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "comment": {"body": {"msg": "Updated"}}, + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a16": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept settings with comment as object", + ), + CommandTestCase( + "comment_int", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a17": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "comment": 42, + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a17": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept settings with comment as int", + ), + CommandTestCase( + "comment_bool", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a18": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "comment": True, + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a18": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept settings with comment as bool", + ), + CommandTestCase( + "comment_array", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a19": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "comment": ["tag1", "tag2"], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a19": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept settings with comment as array", + ), + CommandTestCase( + "comment_null", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a20": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "comment": None, + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a20": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept settings with comment as null", + ), ] diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py index 55e1577bc..4dd1cbc6b 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py @@ -121,6 +121,39 @@ error_code=INVALID_LENGTH_ERROR, msg="setQuerySettings should reject empty hash string", ), + CommandTestCase( + "short_hash_string", + command=lambda ctx: { + "setQuerySettings": "tooshort", + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + error_code=BAD_VALUE_ERROR, + msg="setQuerySettings should reject hash string with wrong length", + ), + CommandTestCase( + "nonhex_hash_string", + command=lambda ctx: { + "setQuerySettings": "ZZZZZZZZ34567890ABCDEF1234567890" + "ABCDEF1234567890ABCDEF1234567890", + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + error_code=BAD_VALUE_ERROR, + msg="setQuerySettings should reject hash string with non-hex chars", + ), CommandTestCase( "indexHints_missing_ns", command=lambda ctx: { @@ -160,6 +193,26 @@ error_code=QUERYSETTINGS_NS_DB_MISSING_ERROR, msg="setQuerySettings should reject indexHints.ns missing db field", ), + CommandTestCase( + "indexHints_ns_null_db", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": None, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + error_code=QUERYSETTINGS_NS_DB_MISSING_ERROR, + msg="setQuerySettings should reject indexHints.ns with null db", + ), CommandTestCase( "indexHints_ns_missing_coll", command=lambda ctx: { @@ -180,6 +233,26 @@ error_code=QUERYSETTINGS_NS_COLL_MISSING_ERROR, msg="setQuerySettings should reject indexHints.ns missing coll field", ), + CommandTestCase( + "indexHints_ns_null_coll", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": None}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + error_code=QUERYSETTINGS_NS_COLL_MISSING_ERROR, + msg="setQuerySettings should reject indexHints.ns with null coll", + ), CommandTestCase( "invalid_query_framework_value", command=lambda ctx: { diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py new file mode 100644 index 000000000..2ae990b22 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py @@ -0,0 +1,595 @@ +"""Tests for setQuerySettings observable effects and verification. + +Validates query shape hash properties, $querySettings stage output for +distinct and aggregate shapes, showDebugQueryShape, multiple settings +management, comment visibility, settings replacement semantics, and +indexHints namespace mismatch acceptance. +""" + +from __future__ import annotations + +import re + +import pytest +from pymongo.collection import Collection + +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command + +from .utils.setQuerySettings_common import cleanup_query_settings, get_query_settings + +# Property [Hash Format]: queryShapeHash is a 64-character hexadecimal string. +# Property [Hash Consistency]: same query shape produces the same hash. +# Property [Hash Uniqueness]: different query shapes produce different hashes. +# Property [Shape Matching]: filter values do not affect shape identity. +# Property [Sort Direction Matters]: different sort directions produce different hashes. +# Property [$querySettings Distinct]: $querySettings returns correct data for distinct. +# Property [$querySettings Aggregate]: $querySettings returns correct data for aggregate. +# Property [showDebugQueryShape True]: debugQueryShape present when requested. +# Property [showDebugQueryShape False]: debugQueryShape absent when not requested. +# Property [Multiple Settings Visible]: all query settings appear in $querySettings. +# Property [Multiple Settings Remove]: removing one leaves others intact. +# Property [Comment Visibility]: settings.comment appears in $querySettings output. +# Property [Comment Update]: updating settings.comment replaces the old value. +# Property [Settings Replacement]: updating settings preserves unmodified fields. +# Property [No Duplicate On Update]: updating same shape does not duplicate entries. +# Property [ns Mismatch]: indexHints ns.coll can differ from query shape collection. + + +def _make_hints(collection: Collection) -> dict: + """Build a standard indexHints settings dict for the given collection.""" + return { + "indexHints": [ + { + "ns": { + "db": collection.database.name, + "coll": collection.name, + }, + "allowedIndexes": ["_id_"], + } + ], + } + + +def _find_entry(collection: Collection, query_hash: str) -> dict: + """Return the $querySettings entry matching the given hash, or {}.""" + settings = get_query_settings(collection) + matching = [s for s in settings if s.get("queryShapeHash") == query_hash] + return matching[0] if matching else {} + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_hash_is_64_char_hex(collection: Collection): + """Test that queryShapeHash is a 64-character hexadecimal string.""" + query = { + "find": collection.name, + "filter": {"h1": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + {"setQuerySettings": query, "settings": _make_hints(collection)}, + ) + h = result.get("queryShapeHash", "") + is_valid = bool(re.fullmatch(r"[0-9A-Fa-f]{64}", h)) + assertSuccessPartial( + {"valid": is_valid}, + {"valid": True}, + msg=f"queryShapeHash should be 64-char hex, got: {h!r}", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_hash_consistent(collection: Collection): + """Test that the same query shape produces the same hash across calls.""" + query = { + "find": collection.name, + "filter": {"h2": 1}, + "$db": collection.database.name, + } + try: + r1 = execute_admin_command( + collection, + {"setQuerySettings": query, "settings": _make_hints(collection)}, + ) + r2 = execute_admin_command( + collection, + {"setQuerySettings": query, "settings": _make_hints(collection)}, + ) + assertSuccessPartial( + r2, + {"queryShapeHash": r1["queryShapeHash"]}, + msg="same query shape should produce identical hashes", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_different_shapes_different_hashes( + collection: Collection, +): + """Test that different query shapes produce different hashes.""" + q1 = { + "find": collection.name, + "filter": {"h3a": 1}, + "$db": collection.database.name, + } + q2 = { + "find": collection.name, + "filter": {"h3b": 1}, + "$db": collection.database.name, + } + try: + r1 = execute_admin_command( + collection, + {"setQuerySettings": q1, "settings": _make_hints(collection)}, + ) + r2 = execute_admin_command( + collection, + {"setQuerySettings": q2, "settings": _make_hints(collection)}, + ) + hashes_differ = r1["queryShapeHash"] != r2["queryShapeHash"] + assertSuccessPartial( + {"differ": hashes_differ}, + {"differ": True}, + msg="different query shapes should produce different hashes", + ) + finally: + cleanup_query_settings(collection, [q1, q2]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_filter_values_do_not_affect_shape( + collection: Collection, +): + """Test that different filter values produce the same query shape hash.""" + q1 = { + "find": collection.name, + "filter": {"x": 1}, + "$db": collection.database.name, + } + q2 = { + "find": collection.name, + "filter": {"x": 999}, + "$db": collection.database.name, + } + try: + r1 = execute_admin_command( + collection, + {"setQuerySettings": q1, "settings": _make_hints(collection)}, + ) + execute_admin_command(collection, {"removeQuerySettings": q1}) + r2 = execute_admin_command( + collection, + {"setQuerySettings": q2, "settings": _make_hints(collection)}, + ) + assertSuccessPartial( + r2, + {"queryShapeHash": r1["queryShapeHash"]}, + msg="filter values should not affect query shape hash", + ) + finally: + cleanup_query_settings(collection, [q2]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_sort_direction_affects_shape( + collection: Collection, +): + """Test that different sort directions produce different hashes.""" + q1 = { + "find": collection.name, + "filter": {"sd": 1}, + "sort": {"a": 1}, + "$db": collection.database.name, + } + q2 = { + "find": collection.name, + "filter": {"sd": 1}, + "sort": {"a": -1}, + "$db": collection.database.name, + } + try: + r1 = execute_admin_command( + collection, + {"setQuerySettings": q1, "settings": _make_hints(collection)}, + ) + r2 = execute_admin_command( + collection, + {"setQuerySettings": q2, "settings": _make_hints(collection)}, + ) + hashes_differ = r1["queryShapeHash"] != r2["queryShapeHash"] + assertSuccessPartial( + {"differ": hashes_differ}, + {"differ": True}, + msg="sort direction should produce different query shape hashes", + ) + finally: + cleanup_query_settings(collection, [q1, q2]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_querySettings_stage_distinct( + collection: Collection, +): + """Test $querySettings returns correct data for a distinct query shape.""" + query = { + "distinct": collection.name, + "key": "x", + "query": {"qs_d1": 1}, + "$db": collection.database.name, + } + try: + r = execute_admin_command( + collection, + {"setQuerySettings": query, "settings": _make_hints(collection)}, + ) + entry = _find_entry(collection, r["queryShapeHash"]) + assertSuccessPartial( + entry.get("representativeQuery", {}), + {"distinct": collection.name}, + msg="representativeQuery should be a distinct shape", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_querySettings_stage_aggregate( + collection: Collection, +): + """Test $querySettings returns correct data for an aggregate query shape.""" + query = { + "aggregate": collection.name, + "pipeline": [{"$match": {"qs_a1": 1}}], + "$db": collection.database.name, + } + try: + r = execute_admin_command( + collection, + {"setQuerySettings": query, "settings": _make_hints(collection)}, + ) + entry = _find_entry(collection, r["queryShapeHash"]) + assertSuccessPartial( + entry.get("representativeQuery", {}), + {"aggregate": collection.name}, + msg="representativeQuery should be an aggregate shape", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_show_debug_query_shape_true(collection: Collection): + """Test debugQueryShape present when showDebugQueryShape is true.""" + query = { + "find": collection.name, + "filter": {"dbg1": 1}, + "$db": collection.database.name, + } + try: + execute_admin_command( + collection, + {"setQuerySettings": query, "settings": _make_hints(collection)}, + ) + settings_true = list( + collection.database.client.admin.aggregate( + [{"$querySettings": {"showDebugQueryShape": True}}] + ) + ) + entry = [ + s + for s in settings_true + if s.get("representativeQuery", {}).get("filter", {}).get("dbg1") + ] + has_debug = "debugQueryShape" in (entry[0] if entry else {}) + assertSuccessPartial( + {"has_debug": has_debug}, + {"has_debug": True}, + msg="debugQueryShape should be present with showDebugQueryShape: true", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_show_debug_query_shape_false(collection: Collection): + """Test debugQueryShape absent when showDebugQueryShape is false.""" + query = { + "find": collection.name, + "filter": {"dbg2": 1}, + "$db": collection.database.name, + } + try: + execute_admin_command( + collection, + {"setQuerySettings": query, "settings": _make_hints(collection)}, + ) + settings_false = list( + collection.database.client.admin.aggregate( + [{"$querySettings": {"showDebugQueryShape": False}}] + ) + ) + entry = [ + s + for s in settings_false + if s.get("representativeQuery", {}).get("filter", {}).get("dbg2") + ] + has_debug = "debugQueryShape" in (entry[0] if entry else {}) + assertSuccessPartial( + {"has_debug": has_debug}, + {"has_debug": False}, + msg="debugQueryShape should be absent with showDebugQueryShape: false", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_multiple_settings_all_visible( + collection: Collection, +): + """Test that three query settings are all visible in $querySettings.""" + q1 = { + "find": collection.name, + "filter": {"multi1": 1}, + "$db": collection.database.name, + } + q2 = { + "find": collection.name, + "filter": {"multi2": 1}, + "$db": collection.database.name, + } + q3 = { + "find": collection.name, + "filter": {"multi3": 1}, + "$db": collection.database.name, + } + try: + r1 = execute_admin_command( + collection, + {"setQuerySettings": q1, "settings": _make_hints(collection)}, + ) + r2 = execute_admin_command( + collection, + {"setQuerySettings": q2, "settings": _make_hints(collection)}, + ) + r3 = execute_admin_command( + collection, + {"setQuerySettings": q3, "settings": _make_hints(collection)}, + ) + all_hashes = {s.get("queryShapeHash") for s in get_query_settings(collection)} + all_present = ( + r1["queryShapeHash"] in all_hashes + and r2["queryShapeHash"] in all_hashes + and r3["queryShapeHash"] in all_hashes + ) + assertSuccessPartial( + {"all_present": all_present}, + {"all_present": True}, + msg="all 3 query settings should be visible in $querySettings", + ) + finally: + cleanup_query_settings(collection, [q1, q2, q3]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_remove_one_leaves_others(collection: Collection): + """Test that removing one setting leaves the others intact.""" + q1 = { + "find": collection.name, + "filter": {"rem1": 1}, + "$db": collection.database.name, + } + q2 = { + "find": collection.name, + "filter": {"rem2": 1}, + "$db": collection.database.name, + } + try: + r1 = execute_admin_command( + collection, + {"setQuerySettings": q1, "settings": _make_hints(collection)}, + ) + r2 = execute_admin_command( + collection, + {"setQuerySettings": q2, "settings": _make_hints(collection)}, + ) + execute_admin_command(collection, {"removeQuerySettings": q1}) + remaining = {s.get("queryShapeHash") for s in get_query_settings(collection)} + correct = r1["queryShapeHash"] not in remaining and r2["queryShapeHash"] in remaining + assertSuccessPartial( + {"correct": correct}, + {"correct": True}, + msg="q1 removed, q2 should remain in $querySettings", + ) + finally: + cleanup_query_settings(collection, [q1, q2]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_comment_visible_in_querySettings( + collection: Collection, +): + """Test that settings.comment appears in $querySettings output.""" + query = { + "find": collection.name, + "filter": {"comvis1": 1}, + "$db": collection.database.name, + } + try: + r = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": {**_make_hints(collection), "comment": "my-test-comment"}, + }, + ) + entry = _find_entry(collection, r["queryShapeHash"]) + assertSuccessPartial( + entry.get("settings", {}), + {"comment": "my-test-comment"}, + msg="comment should be visible in $querySettings output", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_comment_update(collection: Collection): + """Test that updating settings.comment replaces the old value.""" + query = { + "find": collection.name, + "filter": {"comup1": 1}, + "$db": collection.database.name, + } + try: + execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": {**_make_hints(collection), "comment": "original"}, + }, + ) + r = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": {**_make_hints(collection), "comment": "updated"}, + }, + ) + entry = _find_entry(collection, r["queryShapeHash"]) + assertSuccessPartial( + entry.get("settings", {}), + {"comment": "updated"}, + msg="comment should be replaced by the updated value", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_settings_replacement_preserves_fields( + collection: Collection, +): + """Test that updating settings preserves unmodified sub-fields.""" + query = { + "find": collection.name, + "filter": {"rep1": 1}, + "$db": collection.database.name, + } + try: + execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + **_make_hints(collection), + "queryFramework": "classic", + }, + }, + ) + r = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": _make_hints(collection), + }, + ) + entry = _find_entry(collection, r["queryShapeHash"]) + assertSuccessPartial( + entry.get("settings", {}), + {"queryFramework": "classic"}, + msg="queryFramework should be preserved after update with only indexHints", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_no_duplicate_on_update(collection: Collection): + """Test that updating same shape does not create a duplicate entry.""" + query = { + "find": collection.name, + "filter": {"dup1": 1}, + "$db": collection.database.name, + } + try: + r1 = execute_admin_command( + collection, + {"setQuerySettings": query, "settings": _make_hints(collection)}, + ) + execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + **_make_hints(collection), + "queryFramework": "classic", + }, + }, + ) + all_settings = get_query_settings(collection) + count = sum(1 for s in all_settings if s.get("queryShapeHash") == r1["queryShapeHash"]) + assertSuccessPartial( + {"count": count}, + {"count": 1}, + msg="updating same shape should not create duplicate entries", + ) + finally: + cleanup_query_settings(collection, [query]) + + +@pytest.mark.admin +@pytest.mark.replica_set +def test_setQuerySettings_ns_coll_mismatch_accepted(collection: Collection): + """Test that indexHints ns.coll can differ from query shape collection.""" + query = { + "find": collection.name, + "filter": {"mis1": 1}, + "$db": collection.database.name, + } + try: + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": { + "db": collection.database.name, + "coll": "completely_different_collection", + }, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="ns.coll mismatch should be accepted", + ) + finally: + cleanup_query_settings(collection, [query]) From 9f57ae7fb838180b363235b9f1a30c0206c6b7bc Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 15:46:33 -0700 Subject: [PATCH 09/27] Use CommandTestCase Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_reject.py | 409 +++--- .../test_setQuerySettings_verification.py | 1119 ++++++++++------- 2 files changed, 922 insertions(+), 606 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py index 5b1a34cca..d858d4acf 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py @@ -8,13 +8,15 @@ from __future__ import annotations import pytest -from pymongo.collection import Collection +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) from documentdb_tests.framework.assertions import assertResult, assertSuccessPartial from documentdb_tests.framework.error_codes import QUERYSETTINGS_QUERY_REJECTED_ERROR from documentdb_tests.framework.executor import execute_admin_command, execute_command - -from .utils.setQuerySettings_common import cleanup_query_settings +from documentdb_tests.framework.parametrize import pytest_params # Property [Reject Blocks Distinct]: reject: true blocks matching distinct queries. # Property [Reject Blocks Aggregate]: reject: true blocks matching aggregate queries. @@ -23,211 +25,250 @@ # Property [Reject Reversal via Remove]: removing the query setting re-enables the query. # Property [Reject False Succeeds]: reject: false with indexHints allows the query. - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_reject_blocks_distinct(collection: Collection): - """Test that reject: true blocks a matching distinct query.""" - query = { - "distinct": collection.name, - "key": "x", - "query": {"rej_d1": 1}, - "$db": collection.database.name, - } - try: - execute_admin_command( - collection, - {"setQuerySettings": query, "settings": {"reject": True}}, - ) - result = execute_command( - collection, +SET_QUERY_SETTINGS_REJECT_ERROR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "reject_blocks_distinct", + setup_commands=lambda ctx: [ { - "distinct": collection.name, - "key": "x", - "query": {"rej_d1": 1}, - }, - ) - assertResult( - result, - error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, - msg="distinct query matching reject: true should be rejected", - ) - finally: - cleanup_query_settings(collection, [query]) + "setQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "query": {"rej_d1": 1}, + "$db": ctx.database, + }, + "settings": {"reject": True}, + } + ], + command=lambda ctx: { + "distinct": ctx.collection, + "key": "x", + "query": {"rej_d1": 1}, + }, + error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "query": {"rej_d1": 1}, + "$db": ctx.database, + } + } + ], + msg="distinct query matching reject: true should be rejected", + ), + CommandTestCase( + "reject_blocks_aggregate", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"rej_a1": 1}}], + "$db": ctx.database, + }, + "settings": {"reject": True}, + } + ], + command=lambda ctx: { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"rej_a1": 1}}], + "cursor": {}, + }, + error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"rej_a1": 1}}], + "$db": ctx.database, + } + } + ], + msg="aggregate query matching reject: true should be rejected", + ), +] -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_reject_blocks_aggregate(collection: Collection): - """Test that reject: true blocks a matching aggregate query.""" - query = { - "aggregate": collection.name, - "pipeline": [{"$match": {"rej_a1": 1}}], - "$db": collection.database.name, - } - try: - execute_admin_command( - collection, - {"setQuerySettings": query, "settings": {"reject": True}}, - ) - result = execute_command( - collection, +SET_QUERY_SETTINGS_REJECT_SUCCESS_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "reject_does_not_affect_different_shape", + setup_commands=lambda ctx: [ { - "aggregate": collection.name, - "pipeline": [{"$match": {"rej_a1": 1}}], - "cursor": {}, + "setQuerySettings": { + "find": ctx.collection, + "filter": {"rej_s1": 1}, + "$db": ctx.database, + }, + "settings": {"reject": True}, + } + ], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"rej_s2": 1}, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"rej_s1": 1}, + "$db": ctx.database, + } + } + ], + msg="different query shape should not be rejected", + ), + CommandTestCase( + "reject_reversed_by_update", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"rej_u1": 1}, + "$db": ctx.database, + }, + "settings": {"reject": True}, }, - ) - assertResult( - result, - error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, - msg="aggregate query matching reject: true should be rejected", - ) - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_reject_does_not_affect_different_shape( - collection: Collection, -): - """Test that reject: true for one shape does not reject a different shape.""" - query = { - "find": collection.name, - "filter": {"rej_s1": 1}, - "$db": collection.database.name, - } - try: - execute_admin_command( - collection, - {"setQuerySettings": query, "settings": {"reject": True}}, - ) - result = execute_command( - collection, - {"find": collection.name, "filter": {"rej_s2": 1}}, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="different query shape should not be rejected", - ) - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_reject_reversed_by_update(collection: Collection): - """Test that updating reject from true to false re-enables the query.""" - query = { - "find": collection.name, - "filter": {"rej_u1": 1}, - "$db": collection.database.name, - } - try: - execute_admin_command( - collection, - {"setQuerySettings": query, "settings": {"reject": True}}, - ) - execute_admin_command( - collection, { - "setQuerySettings": query, + "setQuerySettings": { + "find": ctx.collection, + "filter": {"rej_u1": 1}, + "$db": ctx.database, + }, "settings": { "reject": False, "indexHints": [ { - "ns": { - "db": collection.database.name, - "coll": collection.name, - }, + "ns": {"db": ctx.database, "coll": ctx.collection}, "allowedIndexes": ["_id_"], } ], }, }, - ) - result = execute_command( - collection, - {"find": collection.name, "filter": {"rej_u1": 1}}, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="query should succeed after reject updated to false", - ) - finally: - cleanup_query_settings(collection, [query]) + ], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"rej_u1": 1}, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"rej_u1": 1}, + "$db": ctx.database, + } + } + ], + msg="query should succeed after reject updated to false", + ), + CommandTestCase( + "reject_reversed_by_remove", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"rej_r1": 1}, + "$db": ctx.database, + }, + "settings": {"reject": True}, + }, + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"rej_r1": 1}, + "$db": ctx.database, + } + }, + ], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"rej_r1": 1}, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"rej_r1": 1}, + "$db": ctx.database, + } + } + ], + msg="query should succeed after removeQuerySettings", + ), + CommandTestCase( + "reject_false_with_hints", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"rej_f1": 1}, + "$db": ctx.database, + }, + "settings": { + "reject": False, + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + } + ], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"rej_f1": 1}, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"rej_f1": 1}, + "$db": ctx.database, + } + } + ], + msg="query with reject: false should succeed", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_reject_reversed_by_remove(collection: Collection): - """Test that removeQuerySettings re-enables a previously rejected query.""" - query = { - "find": collection.name, - "filter": {"rej_r1": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_REJECT_ERROR_TESTS)) +def test_setQuerySettings_reject_errors(collection, test): + """Test that reject: true blocks matching queries.""" + ctx = CommandContext.from_collection(collection) try: - execute_admin_command( - collection, - {"setQuerySettings": query, "settings": {"reject": True}}, - ) - execute_admin_command( - collection, - {"removeQuerySettings": query}, - ) - result = execute_command( - collection, - {"find": collection.name, "filter": {"rej_r1": 1}}, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="query should succeed after removeQuerySettings", - ) + for cmd in test.build_setup(ctx): + execute_admin_command(collection, cmd) + result = execute_command(collection, test.build_command(ctx)) + assertResult(result, error_code=test.error_code, msg=test.msg) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_reject_false_with_hints(collection: Collection): - """Test that reject: false with indexHints allows the matching query.""" - query = { - "find": collection.name, - "filter": {"rej_f1": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_REJECT_SUCCESS_TESTS)) +def test_setQuerySettings_reject_success(collection, test): + """Test that reject scope and reversal work correctly.""" + ctx = CommandContext.from_collection(collection) try: - execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "reject": False, - "indexHints": [ - { - "ns": { - "db": collection.database.name, - "coll": collection.name, - }, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - result = execute_command( - collection, - {"find": collection.name, "filter": {"rej_f1": 1}}, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="query with reject: false should succeed", - ) + for cmd in test.build_setup(ctx): + execute_admin_command(collection, cmd) + result = execute_command(collection, test.build_command(ctx)) + assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) finally: - cleanup_query_settings(collection, [query]) + 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/setQuerySettings/test_setQuerySettings_verification.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py index 2ae990b22..d205b748a 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py @@ -11,10 +11,14 @@ import re import pytest -from pymongo.collection import Collection +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 from .utils.setQuerySettings_common import cleanup_query_settings, get_query_settings @@ -36,560 +40,831 @@ # Property [ns Mismatch]: indexHints ns.coll can differ from query shape collection. -def _make_hints(collection: Collection) -> dict: - """Build a standard indexHints settings dict for the given collection.""" +def _hints(ctx: CommandContext) -> dict: + """Build a standard indexHints settings dict.""" return { "indexHints": [ { - "ns": { - "db": collection.database.name, - "coll": collection.name, - }, + "ns": {"db": ctx.database, "coll": ctx.collection}, "allowedIndexes": ["_id_"], } ], } -def _find_entry(collection: Collection, query_hash: str) -> dict: +def _find_entry(collection, query_hash): """Return the $querySettings entry matching the given hash, or {}.""" settings = get_query_settings(collection) matching = [s for s in settings if s.get("queryShapeHash") == query_hash] return matching[0] if matching else {} -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_hash_is_64_char_hex(collection: Collection): - """Test that queryShapeHash is a 64-character hexadecimal string.""" - query = { - "find": collection.name, - "filter": {"h1": 1}, - "$db": collection.database.name, - } - try: - result = execute_admin_command( - collection, - {"setQuerySettings": query, "settings": _make_hints(collection)}, - ) - h = result.get("queryShapeHash", "") - is_valid = bool(re.fullmatch(r"[0-9A-Fa-f]{64}", h)) - assertSuccessPartial( - {"valid": is_valid}, - {"valid": True}, - msg=f"queryShapeHash should be 64-char hex, got: {h!r}", - ) - finally: - cleanup_query_settings(collection, [query]) +# --------------------------------------------------------------------------- +# Group 1: setQuerySettings response tests (standard CommandTestCase) +# --------------------------------------------------------------------------- + +SET_QUERY_SETTINGS_RESPONSE_CHECK_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "ns_coll_mismatch_accepted", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"mis1": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": { + "db": ctx.database, + "coll": "completely_different_collection", + }, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"mis1": 1}, + "$db": ctx.database, + } + } + ], + msg="ns.coll mismatch should be accepted", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_hash_consistent(collection: Collection): - """Test that the same query shape produces the same hash across calls.""" - query = { - "find": collection.name, - "filter": {"h2": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_RESPONSE_CHECK_TESTS)) +def test_setQuerySettings_response_check(collection, test): + """Test setQuerySettings response for direct-check cases.""" + ctx = CommandContext.from_collection(collection) try: - r1 = execute_admin_command( - collection, - {"setQuerySettings": query, "settings": _make_hints(collection)}, - ) - r2 = execute_admin_command( - collection, - {"setQuerySettings": query, "settings": _make_hints(collection)}, - ) - assertSuccessPartial( - r2, - {"queryShapeHash": r1["queryShapeHash"]}, - msg="same query shape should produce identical hashes", - ) + result = execute_admin_command(collection, test.build_command(ctx)) + assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_different_shapes_different_hashes( - collection: Collection, -): - """Test that different query shapes produce different hashes.""" - q1 = { - "find": collection.name, - "filter": {"h3a": 1}, - "$db": collection.database.name, - } - q2 = { - "find": collection.name, - "filter": {"h3b": 1}, - "$db": collection.database.name, - } - try: - r1 = execute_admin_command( - collection, - {"setQuerySettings": q1, "settings": _make_hints(collection)}, - ) - r2 = execute_admin_command( - collection, - {"setQuerySettings": q2, "settings": _make_hints(collection)}, - ) - hashes_differ = r1["queryShapeHash"] != r2["queryShapeHash"] - assertSuccessPartial( - {"differ": hashes_differ}, - {"differ": True}, - msg="different query shapes should produce different hashes", - ) - finally: - cleanup_query_settings(collection, [q1, q2]) +# --------------------------------------------------------------------------- +# Group 2: Hash property tests +# --------------------------------------------------------------------------- + +SET_QUERY_SETTINGS_HASH_SAME_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "hash_consistent", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"h2": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + } + ], + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"h2": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + }, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"h2": 1}, + "$db": ctx.database, + } + } + ], + msg="same query shape should produce identical hashes", + ), + CommandTestCase( + "filter_values_do_not_affect_shape", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + }, + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "$db": ctx.database, + } + }, + ], + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"x": 999}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + }, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"x": 999}, + "$db": ctx.database, + } + } + ], + msg="filter values should not affect query shape hash", + ), +] + +SET_QUERY_SETTINGS_HASH_DIFFERENT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "different_shapes_different_hashes", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"h3a": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + } + ], + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"h3b": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + }, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"h3a": 1}, + "$db": ctx.database, + } + }, + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"h3b": 1}, + "$db": ctx.database, + } + }, + ], + msg="different query shapes should produce different hashes", + ), + CommandTestCase( + "sort_direction_affects_shape", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"sd": 1}, + "sort": {"a": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + } + ], + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"sd": 1}, + "sort": {"a": -1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + }, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"sd": 1}, + "sort": {"a": 1}, + "$db": ctx.database, + } + }, + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"sd": 1}, + "sort": {"a": -1}, + "$db": ctx.database, + } + }, + ], + msg="sort direction should produce different query shape hashes", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_filter_values_do_not_affect_shape( - collection: Collection, -): - """Test that different filter values produce the same query shape hash.""" - q1 = { - "find": collection.name, - "filter": {"x": 1}, - "$db": collection.database.name, - } - q2 = { - "find": collection.name, - "filter": {"x": 999}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_HASH_SAME_TESTS)) +def test_setQuerySettings_hash_same(collection, test): + """Test that equivalent query shapes produce the same hash.""" + ctx = CommandContext.from_collection(collection) try: - r1 = execute_admin_command( - collection, - {"setQuerySettings": q1, "settings": _make_hints(collection)}, - ) - execute_admin_command(collection, {"removeQuerySettings": q1}) - r2 = execute_admin_command( - collection, - {"setQuerySettings": q2, "settings": _make_hints(collection)}, - ) + setup_hash = None + for cmd in test.build_setup(ctx): + r = execute_admin_command(collection, cmd) + if "queryShapeHash" in r: + setup_hash = r["queryShapeHash"] + result = execute_admin_command(collection, test.build_command(ctx)) assertSuccessPartial( - r2, - {"queryShapeHash": r1["queryShapeHash"]}, - msg="filter values should not affect query shape hash", + result, + {"queryShapeHash": setup_hash}, + msg=test.msg, ) finally: - cleanup_query_settings(collection, [q2]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_sort_direction_affects_shape( - collection: Collection, -): - """Test that different sort directions produce different hashes.""" - q1 = { - "find": collection.name, - "filter": {"sd": 1}, - "sort": {"a": 1}, - "$db": collection.database.name, - } - q2 = { - "find": collection.name, - "filter": {"sd": 1}, - "sort": {"a": -1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_HASH_DIFFERENT_TESTS)) +def test_setQuerySettings_hash_different(collection, test): + """Test that distinct query shapes produce different hashes.""" + ctx = CommandContext.from_collection(collection) try: - r1 = execute_admin_command( - collection, - {"setQuerySettings": q1, "settings": _make_hints(collection)}, - ) - r2 = execute_admin_command( - collection, - {"setQuerySettings": q2, "settings": _make_hints(collection)}, - ) - hashes_differ = r1["queryShapeHash"] != r2["queryShapeHash"] + setup_hash = None + for cmd in test.build_setup(ctx): + r = execute_admin_command(collection, cmd) + if "queryShapeHash" in r: + setup_hash = r["queryShapeHash"] + result = execute_admin_command(collection, test.build_command(ctx)) + hashes_differ = result["queryShapeHash"] != setup_hash assertSuccessPartial( {"differ": hashes_differ}, {"differ": True}, - msg="sort direction should produce different query shape hashes", + msg=test.msg, ) finally: - cleanup_query_settings(collection, [q1, q2]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass + + +# --------------------------------------------------------------------------- +# Group 3: Hash format test (standalone — regex check on response) +# --------------------------------------------------------------------------- @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_querySettings_stage_distinct( - collection: Collection, -): - """Test $querySettings returns correct data for a distinct query shape.""" +def test_setQuerySettings_hash_is_64_char_hex(collection): + """Test that queryShapeHash is a 64-character hexadecimal string.""" + ctx = CommandContext.from_collection(collection) query = { - "distinct": collection.name, - "key": "x", - "query": {"qs_d1": 1}, - "$db": collection.database.name, + "find": ctx.collection, + "filter": {"h1": 1}, + "$db": ctx.database, } try: - r = execute_admin_command( + result = execute_admin_command( collection, - {"setQuerySettings": query, "settings": _make_hints(collection)}, + {"setQuerySettings": query, "settings": _hints(ctx)}, ) - entry = _find_entry(collection, r["queryShapeHash"]) + h = result.get("queryShapeHash", "") + is_valid = bool(re.fullmatch(r"[0-9A-Fa-f]{64}", h)) assertSuccessPartial( - entry.get("representativeQuery", {}), - {"distinct": collection.name}, - msg="representativeQuery should be a distinct shape", + {"valid": is_valid}, + {"valid": True}, + msg=f"queryShapeHash should be 64-char hex, got: {h!r}", ) finally: cleanup_query_settings(collection, [query]) +# --------------------------------------------------------------------------- +# Group 4: $querySettings inspection tests +# --------------------------------------------------------------------------- + +SET_QUERY_SETTINGS_QS_STAGE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "querySettings_stage_distinct", + command=lambda ctx: { + "setQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "query": {"qs_d1": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + }, + expected=lambda ctx: {"distinct": ctx.collection}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "query": {"qs_d1": 1}, + "$db": ctx.database, + } + } + ], + msg="representativeQuery should be a distinct shape", + ), + CommandTestCase( + "querySettings_stage_aggregate", + command=lambda ctx: { + "setQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"qs_a1": 1}}], + "$db": ctx.database, + }, + "settings": _hints(ctx), + }, + expected=lambda ctx: {"aggregate": ctx.collection}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"qs_a1": 1}}], + "$db": ctx.database, + } + } + ], + msg="representativeQuery should be an aggregate shape", + ), +] + + @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_querySettings_stage_aggregate( - collection: Collection, -): - """Test $querySettings returns correct data for an aggregate query shape.""" - query = { - "aggregate": collection.name, - "pipeline": [{"$match": {"qs_a1": 1}}], - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_QS_STAGE_TESTS)) +def test_setQuerySettings_qs_stage(collection, test): + """Test $querySettings returns correct representativeQuery.""" + ctx = CommandContext.from_collection(collection) try: - r = execute_admin_command( - collection, - {"setQuerySettings": query, "settings": _make_hints(collection)}, - ) + r = execute_admin_command(collection, test.build_command(ctx)) entry = _find_entry(collection, r["queryShapeHash"]) assertSuccessPartial( entry.get("representativeQuery", {}), - {"aggregate": collection.name}, - msg="representativeQuery should be an aggregate shape", + test.build_expected(ctx), + msg=test.msg, ) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass + + +# --------------------------------------------------------------------------- +# Group 5: showDebugQueryShape tests +# --------------------------------------------------------------------------- + +SET_QUERY_SETTINGS_DEBUG_SHAPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "show_debug_query_shape_true", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"dbg1": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + }, + expected={"has_debug": True}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"dbg1": 1}, + "$db": ctx.database, + } + } + ], + msg="debugQueryShape should be present with showDebugQueryShape: true", + ), + CommandTestCase( + "show_debug_query_shape_false", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"dbg2": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + }, + expected={"has_debug": False}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"dbg2": 1}, + "$db": ctx.database, + } + } + ], + msg="debugQueryShape should be absent with showDebugQueryShape: false", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_show_debug_query_shape_true(collection: Collection): - """Test debugQueryShape present when showDebugQueryShape is true.""" - query = { - "find": collection.name, - "filter": {"dbg1": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_DEBUG_SHAPE_TESTS)) +def test_setQuerySettings_debug_shape(collection, test): + """Test showDebugQueryShape controls debugQueryShape presence.""" + ctx = CommandContext.from_collection(collection) + expected = test.build_expected(ctx) + show_debug = expected["has_debug"] try: - execute_admin_command( - collection, - {"setQuerySettings": query, "settings": _make_hints(collection)}, - ) - settings_true = list( + execute_admin_command(collection, test.build_command(ctx)) + settings = list( collection.database.client.admin.aggregate( - [{"$querySettings": {"showDebugQueryShape": True}}] + [{"$querySettings": {"showDebugQueryShape": show_debug}}] ) ) + filter_key = "dbg1" if show_debug else "dbg2" entry = [ s - for s in settings_true - if s.get("representativeQuery", {}).get("filter", {}).get("dbg1") + for s in settings + if s.get("representativeQuery", {}).get("filter", {}).get(filter_key) ] has_debug = "debugQueryShape" in (entry[0] if entry else {}) assertSuccessPartial( {"has_debug": has_debug}, - {"has_debug": True}, - msg="debugQueryShape should be present with showDebugQueryShape: true", + expected, + msg=test.msg, ) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass + + +# --------------------------------------------------------------------------- +# Group 6: Comment visibility tests +# --------------------------------------------------------------------------- + +SET_QUERY_SETTINGS_COMMENT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "comment_visible_in_querySettings", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"comvis1": 1}, + "$db": ctx.database, + }, + "settings": {**_hints(ctx), "comment": "my-test-comment"}, + }, + expected={"comment": "my-test-comment"}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"comvis1": 1}, + "$db": ctx.database, + } + } + ], + msg="comment should be visible in $querySettings output", + ), + CommandTestCase( + "comment_update", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"comup1": 1}, + "$db": ctx.database, + }, + "settings": {**_hints(ctx), "comment": "original"}, + } + ], + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"comup1": 1}, + "$db": ctx.database, + }, + "settings": {**_hints(ctx), "comment": "updated"}, + }, + expected={"comment": "updated"}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"comup1": 1}, + "$db": ctx.database, + } + } + ], + msg="comment should be replaced by the updated value", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_show_debug_query_shape_false(collection: Collection): - """Test debugQueryShape absent when showDebugQueryShape is false.""" - query = { - "find": collection.name, - "filter": {"dbg2": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_COMMENT_TESTS)) +def test_setQuerySettings_comment(collection, test): + """Test settings.comment visibility in $querySettings.""" + ctx = CommandContext.from_collection(collection) try: - execute_admin_command( - collection, - {"setQuerySettings": query, "settings": _make_hints(collection)}, - ) - settings_false = list( - collection.database.client.admin.aggregate( - [{"$querySettings": {"showDebugQueryShape": False}}] - ) - ) - entry = [ - s - for s in settings_false - if s.get("representativeQuery", {}).get("filter", {}).get("dbg2") - ] - has_debug = "debugQueryShape" in (entry[0] if entry else {}) + for cmd in test.build_setup(ctx): + execute_admin_command(collection, cmd) + r = execute_admin_command(collection, test.build_command(ctx)) + entry = _find_entry(collection, r["queryShapeHash"]) assertSuccessPartial( - {"has_debug": has_debug}, - {"has_debug": False}, - msg="debugQueryShape should be absent with showDebugQueryShape: false", + entry.get("settings", {}), + test.build_expected(ctx), + msg=test.msg, ) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_multiple_settings_all_visible( - collection: Collection, -): - """Test that three query settings are all visible in $querySettings.""" - q1 = { - "find": collection.name, - "filter": {"multi1": 1}, - "$db": collection.database.name, - } - q2 = { - "find": collection.name, - "filter": {"multi2": 1}, - "$db": collection.database.name, - } - q3 = { - "find": collection.name, - "filter": {"multi3": 1}, - "$db": collection.database.name, - } - try: - r1 = execute_admin_command( - collection, - {"setQuerySettings": q1, "settings": _make_hints(collection)}, - ) - r2 = execute_admin_command( - collection, - {"setQuerySettings": q2, "settings": _make_hints(collection)}, - ) - r3 = execute_admin_command( - collection, - {"setQuerySettings": q3, "settings": _make_hints(collection)}, - ) - all_hashes = {s.get("queryShapeHash") for s in get_query_settings(collection)} - all_present = ( - r1["queryShapeHash"] in all_hashes - and r2["queryShapeHash"] in all_hashes - and r3["queryShapeHash"] in all_hashes - ) - assertSuccessPartial( - {"all_present": all_present}, - {"all_present": True}, - msg="all 3 query settings should be visible in $querySettings", - ) - finally: - cleanup_query_settings(collection, [q1, q2, q3]) +# --------------------------------------------------------------------------- +# Group 7: Settings replacement / update tests +# --------------------------------------------------------------------------- + +SET_QUERY_SETTINGS_UPDATE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "settings_replacement_preserves_fields", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"rep1": 1}, + "$db": ctx.database, + }, + "settings": {**_hints(ctx), "queryFramework": "classic"}, + } + ], + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"rep1": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + }, + expected={"queryFramework": "classic"}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"rep1": 1}, + "$db": ctx.database, + } + } + ], + msg="queryFramework should be preserved after update with only indexHints", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_remove_one_leaves_others(collection: Collection): - """Test that removing one setting leaves the others intact.""" - q1 = { - "find": collection.name, - "filter": {"rem1": 1}, - "$db": collection.database.name, - } - q2 = { - "find": collection.name, - "filter": {"rem2": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_UPDATE_TESTS)) +def test_setQuerySettings_update(collection, test): + """Test settings update semantics.""" + ctx = CommandContext.from_collection(collection) try: - r1 = execute_admin_command( - collection, - {"setQuerySettings": q1, "settings": _make_hints(collection)}, - ) - r2 = execute_admin_command( - collection, - {"setQuerySettings": q2, "settings": _make_hints(collection)}, - ) - execute_admin_command(collection, {"removeQuerySettings": q1}) - remaining = {s.get("queryShapeHash") for s in get_query_settings(collection)} - correct = r1["queryShapeHash"] not in remaining and r2["queryShapeHash"] in remaining + for cmd in test.build_setup(ctx): + execute_admin_command(collection, cmd) + r = execute_admin_command(collection, test.build_command(ctx)) + entry = _find_entry(collection, r["queryShapeHash"]) assertSuccessPartial( - {"correct": correct}, - {"correct": True}, - msg="q1 removed, q2 should remain in $querySettings", + entry.get("settings", {}), + test.build_expected(ctx), + msg=test.msg, ) finally: - cleanup_query_settings(collection, [q1, q2]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_comment_visible_in_querySettings( - collection: Collection, -): - """Test that settings.comment appears in $querySettings output.""" - query = { - "find": collection.name, - "filter": {"comvis1": 1}, - "$db": collection.database.name, - } - try: - r = execute_admin_command( - collection, +# --------------------------------------------------------------------------- +# Group 8: No duplicate on update test +# --------------------------------------------------------------------------- + +SET_QUERY_SETTINGS_NO_DUPLICATE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "no_duplicate_on_update", + setup_commands=lambda ctx: [ { - "setQuerySettings": query, - "settings": {**_make_hints(collection), "comment": "my-test-comment"}, + "setQuerySettings": { + "find": ctx.collection, + "filter": {"dup1": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + } + ], + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"dup1": 1}, + "$db": ctx.database, }, - ) - entry = _find_entry(collection, r["queryShapeHash"]) - assertSuccessPartial( - entry.get("settings", {}), - {"comment": "my-test-comment"}, - msg="comment should be visible in $querySettings output", - ) - finally: - cleanup_query_settings(collection, [query]) + "settings": {**_hints(ctx), "queryFramework": "classic"}, + }, + expected={"count": 1}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"dup1": 1}, + "$db": ctx.database, + } + } + ], + msg="updating same shape should not create duplicate entries", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_comment_update(collection: Collection): - """Test that updating settings.comment replaces the old value.""" - query = { - "find": collection.name, - "filter": {"comup1": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_NO_DUPLICATE_TESTS)) +def test_setQuerySettings_no_duplicate(collection, test): + """Test that updating same shape does not create duplicates.""" + ctx = CommandContext.from_collection(collection) try: - execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": {**_make_hints(collection), "comment": "original"}, - }, - ) - r = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": {**_make_hints(collection), "comment": "updated"}, - }, - ) - entry = _find_entry(collection, r["queryShapeHash"]) + for cmd in test.build_setup(ctx): + execute_admin_command(collection, cmd) + r = execute_admin_command(collection, test.build_command(ctx)) + all_settings = get_query_settings(collection) + count = sum(1 for s in all_settings if s.get("queryShapeHash") == r["queryShapeHash"]) assertSuccessPartial( - entry.get("settings", {}), - {"comment": "updated"}, - msg="comment should be replaced by the updated value", + {"count": count}, + test.build_expected(ctx), + msg=test.msg, ) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_settings_replacement_preserves_fields( - collection: Collection, -): - """Test that updating settings preserves unmodified sub-fields.""" - query = { - "find": collection.name, - "filter": {"rep1": 1}, - "$db": collection.database.name, - } - try: - execute_admin_command( - collection, +# --------------------------------------------------------------------------- +# Group 9: Multiple settings management tests +# --------------------------------------------------------------------------- + +SET_QUERY_SETTINGS_MULTI_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "multiple_settings_all_visible", + setup_commands=lambda ctx: [ { - "setQuerySettings": query, - "settings": { - **_make_hints(collection), - "queryFramework": "classic", + "setQuerySettings": { + "find": ctx.collection, + "filter": {"multi1": 1}, + "$db": ctx.database, }, + "settings": _hints(ctx), }, - ) - r = execute_admin_command( - collection, { - "setQuerySettings": query, - "settings": _make_hints(collection), + "setQuerySettings": { + "find": ctx.collection, + "filter": {"multi2": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), }, - ) - entry = _find_entry(collection, r["queryShapeHash"]) - assertSuccessPartial( - entry.get("settings", {}), - {"queryFramework": "classic"}, - msg="queryFramework should be preserved after update with only indexHints", - ) - finally: - cleanup_query_settings(collection, [query]) + ], + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"multi3": 1}, + "$db": ctx.database, + }, + "settings": _hints(ctx), + }, + expected={"all_present": True}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"multi1": 1}, + "$db": ctx.database, + } + }, + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"multi2": 1}, + "$db": ctx.database, + } + }, + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"multi3": 1}, + "$db": ctx.database, + } + }, + ], + msg="all 3 query settings should be visible in $querySettings", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_no_duplicate_on_update(collection: Collection): - """Test that updating same shape does not create a duplicate entry.""" - query = { - "find": collection.name, - "filter": {"dup1": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_MULTI_TESTS)) +def test_setQuerySettings_multi(collection, test): + """Test that multiple query settings are independently visible.""" + ctx = CommandContext.from_collection(collection) try: - r1 = execute_admin_command( - collection, - {"setQuerySettings": query, "settings": _make_hints(collection)}, - ) - execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - **_make_hints(collection), - "queryFramework": "classic", - }, - }, - ) - all_settings = get_query_settings(collection) - count = sum(1 for s in all_settings if s.get("queryShapeHash") == r1["queryShapeHash"]) + setup_hashes = [] + for cmd in test.build_setup(ctx): + r = execute_admin_command(collection, cmd) + if "queryShapeHash" in r: + setup_hashes.append(r["queryShapeHash"]) + r = execute_admin_command(collection, test.build_command(ctx)) + setup_hashes.append(r["queryShapeHash"]) + all_hashes = {s.get("queryShapeHash") for s in get_query_settings(collection)} + all_present = all(h in all_hashes for h in setup_hashes) assertSuccessPartial( - {"count": count}, - {"count": 1}, - msg="updating same shape should not create duplicate entries", + {"all_present": all_present}, + test.build_expected(ctx), + msg=test.msg, ) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass + + +# --------------------------------------------------------------------------- +# Group 10: Remove one leaves others test (standalone) +# --------------------------------------------------------------------------- @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_ns_coll_mismatch_accepted(collection: Collection): - """Test that indexHints ns.coll can differ from query shape collection.""" - query = { - "find": collection.name, - "filter": {"mis1": 1}, - "$db": collection.database.name, +def test_setQuerySettings_remove_one_leaves_others(collection): + """Test that removing one setting leaves the others intact.""" + ctx = CommandContext.from_collection(collection) + q1 = { + "find": ctx.collection, + "filter": {"rem1": 1}, + "$db": ctx.database, + } + q2 = { + "find": ctx.collection, + "filter": {"rem2": 1}, + "$db": ctx.database, } try: - result = execute_admin_command( + r1 = execute_admin_command( collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": { - "db": collection.database.name, - "coll": "completely_different_collection", - }, - "allowedIndexes": ["_id_"], - } - ], - }, - }, + {"setQuerySettings": q1, "settings": _hints(ctx)}, ) + r2 = execute_admin_command( + collection, + {"setQuerySettings": q2, "settings": _hints(ctx)}, + ) + execute_admin_command(collection, {"removeQuerySettings": q1}) + remaining = {s.get("queryShapeHash") for s in get_query_settings(collection)} + correct = r1["queryShapeHash"] not in remaining and r2["queryShapeHash"] in remaining assertSuccessPartial( - result, - {"ok": 1.0}, - msg="ns.coll mismatch should be accepted", + {"correct": correct}, + {"correct": True}, + msg="q1 removed, q2 should remain in $querySettings", ) finally: - cleanup_query_settings(collection, [query]) + cleanup_query_settings(collection, [q1, q2]) From 51243c0a06b1a3cad18b51041d27a09907e11f17 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 15:52:56 -0700 Subject: [PATCH 10/27] in-line helpers Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_verification.py | 258 ++++++++++++++---- 1 file changed, 211 insertions(+), 47 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py index d205b748a..7c76831ad 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py @@ -40,25 +40,6 @@ # Property [ns Mismatch]: indexHints ns.coll can differ from query shape collection. -def _hints(ctx: CommandContext) -> dict: - """Build a standard indexHints settings dict.""" - return { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - } - - -def _find_entry(collection, query_hash): - """Return the $querySettings entry matching the given hash, or {}.""" - settings = get_query_settings(collection) - matching = [s for s in settings if s.get("queryShapeHash") == query_hash] - return matching[0] if matching else {} - - # --------------------------------------------------------------------------- # Group 1: setQuerySettings response tests (standard CommandTestCase) # --------------------------------------------------------------------------- @@ -130,7 +111,14 @@ def test_setQuerySettings_response_check(collection, test): "filter": {"h2": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, } ], command=lambda ctx: { @@ -139,7 +127,14 @@ def test_setQuerySettings_response_check(collection, test): "filter": {"h2": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, cleanup=lambda ctx: [ { @@ -161,7 +156,14 @@ def test_setQuerySettings_response_check(collection, test): "filter": {"x": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, { "removeQuerySettings": { @@ -177,7 +179,14 @@ def test_setQuerySettings_response_check(collection, test): "filter": {"x": 999}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, cleanup=lambda ctx: [ { @@ -202,7 +211,14 @@ def test_setQuerySettings_response_check(collection, test): "filter": {"h3a": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, } ], command=lambda ctx: { @@ -211,7 +227,14 @@ def test_setQuerySettings_response_check(collection, test): "filter": {"h3b": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, cleanup=lambda ctx: [ { @@ -241,7 +264,14 @@ def test_setQuerySettings_response_check(collection, test): "sort": {"a": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, } ], command=lambda ctx: { @@ -251,7 +281,14 @@ def test_setQuerySettings_response_check(collection, test): "sort": {"a": -1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, cleanup=lambda ctx: [ { @@ -347,7 +384,17 @@ def test_setQuerySettings_hash_is_64_char_hex(collection): try: result = execute_admin_command( collection, - {"setQuerySettings": query, "settings": _hints(ctx)}, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, ) h = result.get("queryShapeHash", "") is_valid = bool(re.fullmatch(r"[0-9A-Fa-f]{64}", h)) @@ -374,7 +421,14 @@ def test_setQuerySettings_hash_is_64_char_hex(collection): "query": {"qs_d1": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected=lambda ctx: {"distinct": ctx.collection}, cleanup=lambda ctx: [ @@ -397,7 +451,14 @@ def test_setQuerySettings_hash_is_64_char_hex(collection): "pipeline": [{"$match": {"qs_a1": 1}}], "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected=lambda ctx: {"aggregate": ctx.collection}, cleanup=lambda ctx: [ @@ -422,7 +483,9 @@ def test_setQuerySettings_qs_stage(collection, test): ctx = CommandContext.from_collection(collection) try: r = execute_admin_command(collection, test.build_command(ctx)) - entry = _find_entry(collection, r["queryShapeHash"]) + settings = get_query_settings(collection) + matching = [s for s in settings if s.get("queryShapeHash") == r["queryShapeHash"]] + entry = matching[0] if matching else {} assertSuccessPartial( entry.get("representativeQuery", {}), test.build_expected(ctx), @@ -449,7 +512,14 @@ def test_setQuerySettings_qs_stage(collection, test): "filter": {"dbg1": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"has_debug": True}, cleanup=lambda ctx: [ @@ -471,7 +541,14 @@ def test_setQuerySettings_qs_stage(collection, test): "filter": {"dbg2": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"has_debug": False}, cleanup=lambda ctx: [ @@ -536,7 +613,15 @@ def test_setQuerySettings_debug_shape(collection, test): "filter": {"comvis1": 1}, "$db": ctx.database, }, - "settings": {**_hints(ctx), "comment": "my-test-comment"}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "comment": "my-test-comment", + }, }, expected={"comment": "my-test-comment"}, cleanup=lambda ctx: [ @@ -559,7 +644,15 @@ def test_setQuerySettings_debug_shape(collection, test): "filter": {"comup1": 1}, "$db": ctx.database, }, - "settings": {**_hints(ctx), "comment": "original"}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "comment": "original", + }, } ], command=lambda ctx: { @@ -568,7 +661,15 @@ def test_setQuerySettings_debug_shape(collection, test): "filter": {"comup1": 1}, "$db": ctx.database, }, - "settings": {**_hints(ctx), "comment": "updated"}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "comment": "updated", + }, }, expected={"comment": "updated"}, cleanup=lambda ctx: [ @@ -595,7 +696,9 @@ def test_setQuerySettings_comment(collection, test): for cmd in test.build_setup(ctx): execute_admin_command(collection, cmd) r = execute_admin_command(collection, test.build_command(ctx)) - entry = _find_entry(collection, r["queryShapeHash"]) + settings = get_query_settings(collection) + matching = [s for s in settings if s.get("queryShapeHash") == r["queryShapeHash"]] + entry = matching[0] if matching else {} assertSuccessPartial( entry.get("settings", {}), test.build_expected(ctx), @@ -623,7 +726,15 @@ def test_setQuerySettings_comment(collection, test): "filter": {"rep1": 1}, "$db": ctx.database, }, - "settings": {**_hints(ctx), "queryFramework": "classic"}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": "classic", + }, } ], command=lambda ctx: { @@ -632,7 +743,14 @@ def test_setQuerySettings_comment(collection, test): "filter": {"rep1": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"queryFramework": "classic"}, cleanup=lambda ctx: [ @@ -659,7 +777,9 @@ def test_setQuerySettings_update(collection, test): for cmd in test.build_setup(ctx): execute_admin_command(collection, cmd) r = execute_admin_command(collection, test.build_command(ctx)) - entry = _find_entry(collection, r["queryShapeHash"]) + settings = get_query_settings(collection) + matching = [s for s in settings if s.get("queryShapeHash") == r["queryShapeHash"]] + entry = matching[0] if matching else {} assertSuccessPartial( entry.get("settings", {}), test.build_expected(ctx), @@ -687,7 +807,14 @@ def test_setQuerySettings_update(collection, test): "filter": {"dup1": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, } ], command=lambda ctx: { @@ -696,7 +823,15 @@ def test_setQuerySettings_update(collection, test): "filter": {"dup1": 1}, "$db": ctx.database, }, - "settings": {**_hints(ctx), "queryFramework": "classic"}, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": "classic", + }, }, expected={"count": 1}, cleanup=lambda ctx: [ @@ -752,7 +887,14 @@ def test_setQuerySettings_no_duplicate(collection, test): "filter": {"multi1": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, { "setQuerySettings": { @@ -760,7 +902,14 @@ def test_setQuerySettings_no_duplicate(collection, test): "filter": {"multi2": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, ], command=lambda ctx: { @@ -769,7 +918,14 @@ def test_setQuerySettings_no_duplicate(collection, test): "filter": {"multi3": 1}, "$db": ctx.database, }, - "settings": _hints(ctx), + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, }, expected={"all_present": True}, cleanup=lambda ctx: [ @@ -849,14 +1005,22 @@ def test_setQuerySettings_remove_one_leaves_others(collection): "filter": {"rem2": 1}, "$db": ctx.database, } + hints = { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + } try: r1 = execute_admin_command( collection, - {"setQuerySettings": q1, "settings": _hints(ctx)}, + {"setQuerySettings": q1, "settings": hints}, ) r2 = execute_admin_command( collection, - {"setQuerySettings": q2, "settings": _hints(ctx)}, + {"setQuerySettings": q2, "settings": hints}, ) execute_admin_command(collection, {"removeQuerySettings": q1}) remaining = {s.get("queryShapeHash") for s in get_query_settings(collection)} From 19f74c6a768db05c9d7ade5e7347dc2e62cbc35f Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 16:12:36 -0700 Subject: [PATCH 11/27] rename tests Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_reject.py | 2 +- .../test_setQuerySettings_verification.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py index d858d4acf..6decf6ddd 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py @@ -198,7 +198,7 @@ msg="query should succeed after removeQuerySettings", ), CommandTestCase( - "reject_false_with_hints", + "reject_false_allows_query", setup_commands=lambda ctx: [ { "setQuerySettings": { diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py index 7c76831ad..369c7727c 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py @@ -103,7 +103,7 @@ def test_setQuerySettings_response_check(collection, test): SET_QUERY_SETTINGS_HASH_SAME_TESTS: list[CommandTestCase] = [ CommandTestCase( - "hash_consistent", + "same_shape_produces_same_hash", setup_commands=lambda ctx: [ { "setQuerySettings": { @@ -413,7 +413,7 @@ def test_setQuerySettings_hash_is_64_char_hex(collection): SET_QUERY_SETTINGS_QS_STAGE_TESTS: list[CommandTestCase] = [ CommandTestCase( - "querySettings_stage_distinct", + "querySettings_returns_distinct_shape", command=lambda ctx: { "setQuerySettings": { "distinct": ctx.collection, @@ -444,7 +444,7 @@ def test_setQuerySettings_hash_is_64_char_hex(collection): msg="representativeQuery should be a distinct shape", ), CommandTestCase( - "querySettings_stage_aggregate", + "querySettings_returns_aggregate_shape", command=lambda ctx: { "setQuerySettings": { "aggregate": ctx.collection, @@ -505,7 +505,7 @@ def test_setQuerySettings_qs_stage(collection, test): SET_QUERY_SETTINGS_DEBUG_SHAPE_TESTS: list[CommandTestCase] = [ CommandTestCase( - "show_debug_query_shape_true", + "debug_query_shape_present_when_enabled", command=lambda ctx: { "setQuerySettings": { "find": ctx.collection, @@ -534,7 +534,7 @@ def test_setQuerySettings_qs_stage(collection, test): msg="debugQueryShape should be present with showDebugQueryShape: true", ), CommandTestCase( - "show_debug_query_shape_false", + "debug_query_shape_absent_when_disabled", command=lambda ctx: { "setQuerySettings": { "find": ctx.collection, @@ -636,7 +636,7 @@ def test_setQuerySettings_debug_shape(collection, test): msg="comment should be visible in $querySettings output", ), CommandTestCase( - "comment_update", + "comment_replaced_on_update", setup_commands=lambda ctx: [ { "setQuerySettings": { @@ -718,7 +718,7 @@ def test_setQuerySettings_comment(collection, test): SET_QUERY_SETTINGS_UPDATE_TESTS: list[CommandTestCase] = [ CommandTestCase( - "settings_replacement_preserves_fields", + "update_preserves_unmodified_fields", setup_commands=lambda ctx: [ { "setQuerySettings": { From 76e2b657fbd25e21045cb1382517df0bbf4d8a0f Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 16:21:06 -0700 Subject: [PATCH 12/27] add tests for special index types Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_settings.py | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py index 536c42e34..463458aa3 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py @@ -27,6 +27,10 @@ # Property [$natural Hint]: setQuerySettings accepts $natural in allowedIndexes. # Property [Multiple indexHints]: setQuerySettings accepts multiple indexHints documents. # Property [Non-Existent Index]: setQuerySettings accepts non-existent index names. +# Property [Text Index Spec]: setQuerySettings accepts text index key pattern in allowedIndexes. +# Property [2dsphere Index Spec]: setQuerySettings accepts 2dsphere index key pattern. +# Property [2d Index Spec]: setQuerySettings accepts 2d index key pattern. +# Property [Hashed Index Spec]: setQuerySettings accepts hashed index key pattern. SET_QUERY_SETTINGS_SETTINGS_TESTS: list[CommandTestCase] = [ CommandTestCase( "indexHints_single_index", @@ -529,6 +533,122 @@ ], msg="should accept settings with comment as null", ), + CommandTestCase( + "indexHints_text_index_spec", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a21": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": [{"a21": "text"}], + } + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a21": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept text index key pattern in allowedIndexes", + ), + CommandTestCase( + "indexHints_2dsphere_index_spec", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a22": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": [{"geo": "2dsphere"}], + } + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a22": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept 2dsphere index key pattern in allowedIndexes", + ), + CommandTestCase( + "indexHints_2d_index_spec", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a23": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": [{"loc": "2d"}], + } + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a23": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept 2d index key pattern in allowedIndexes", + ), + CommandTestCase( + "indexHints_hashed_index_spec", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a24": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": [{"a24": "hashed"}], + } + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a24": 1}, + "$db": ctx.database, + } + } + ], + msg="should accept hashed index key pattern in allowedIndexes", + ), ] From 60cc5a784aafe4604a7ff0b1a1e84f95a071bb24 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 16:27:47 -0700 Subject: [PATCH 13/27] merge test functions into 1 Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_verification.py | 51 ++++--------------- 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py index 369c7727c..ea10dcdb0 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py @@ -601,10 +601,11 @@ def test_setQuerySettings_debug_shape(collection, test): # --------------------------------------------------------------------------- -# Group 6: Comment visibility tests +# Group 6: Settings field verification via $querySettings +# (comment visibility, comment update, settings replacement) # --------------------------------------------------------------------------- -SET_QUERY_SETTINGS_COMMENT_TESTS: list[CommandTestCase] = [ +SET_QUERY_SETTINGS_FIELD_VERIFICATION_TESTS: list[CommandTestCase] = [ CommandTestCase( "comment_visible_in_querySettings", command=lambda ctx: { @@ -683,40 +684,6 @@ def test_setQuerySettings_debug_shape(collection, test): ], msg="comment should be replaced by the updated value", ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_COMMENT_TESTS)) -def test_setQuerySettings_comment(collection, test): - """Test settings.comment visibility in $querySettings.""" - ctx = CommandContext.from_collection(collection) - try: - for cmd in test.build_setup(ctx): - execute_admin_command(collection, cmd) - r = execute_admin_command(collection, test.build_command(ctx)) - settings = get_query_settings(collection) - matching = [s for s in settings if s.get("queryShapeHash") == r["queryShapeHash"]] - entry = matching[0] if matching else {} - assertSuccessPartial( - entry.get("settings", {}), - test.build_expected(ctx), - msg=test.msg, - ) - finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass - - -# --------------------------------------------------------------------------- -# Group 7: Settings replacement / update tests -# --------------------------------------------------------------------------- - -SET_QUERY_SETTINGS_UPDATE_TESTS: list[CommandTestCase] = [ CommandTestCase( "update_preserves_unmodified_fields", setup_commands=lambda ctx: [ @@ -769,9 +736,9 @@ def test_setQuerySettings_comment(collection, test): @pytest.mark.admin @pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_UPDATE_TESTS)) -def test_setQuerySettings_update(collection, test): - """Test settings update semantics.""" +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_FIELD_VERIFICATION_TESTS)) +def test_setQuerySettings_field_verification(collection, test): + """Test settings fields are visible and correctly updated in $querySettings.""" ctx = CommandContext.from_collection(collection) try: for cmd in test.build_setup(ctx): @@ -794,7 +761,7 @@ def test_setQuerySettings_update(collection, test): # --------------------------------------------------------------------------- -# Group 8: No duplicate on update test +# Group 7: No duplicate on update test # --------------------------------------------------------------------------- SET_QUERY_SETTINGS_NO_DUPLICATE_TESTS: list[CommandTestCase] = [ @@ -874,7 +841,7 @@ def test_setQuerySettings_no_duplicate(collection, test): # --------------------------------------------------------------------------- -# Group 9: Multiple settings management tests +# Group 8: Multiple settings management tests # --------------------------------------------------------------------------- SET_QUERY_SETTINGS_MULTI_TESTS: list[CommandTestCase] = [ @@ -986,7 +953,7 @@ def test_setQuerySettings_multi(collection, test): # --------------------------------------------------------------------------- -# Group 10: Remove one leaves others test (standalone) +# Group 9: Remove one leaves others test (standalone) # --------------------------------------------------------------------------- From dd8614440f5d0932ccc9ac3a9f5cba3e7bf5238d Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 16:33:57 -0700 Subject: [PATCH 14/27] use standalone test cases Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_verification.py | 290 +++++------------- 1 file changed, 84 insertions(+), 206 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py index ea10dcdb0..073f82b10 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py @@ -41,60 +41,45 @@ # --------------------------------------------------------------------------- -# Group 1: setQuerySettings response tests (standard CommandTestCase) +# Group 1: ns.coll mismatch acceptance test (standalone) # --------------------------------------------------------------------------- -SET_QUERY_SETTINGS_RESPONSE_CHECK_TESTS: list[CommandTestCase] = [ - CommandTestCase( - "ns_coll_mismatch_accepted", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"mis1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": { - "db": ctx.database, - "coll": "completely_different_collection", - }, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"mis1": 1}, - "$db": ctx.database, - } - } - ], - msg="ns.coll mismatch should be accepted", - ), -] - @pytest.mark.admin @pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_RESPONSE_CHECK_TESTS)) -def test_setQuerySettings_response_check(collection, test): - """Test setQuerySettings response for direct-check cases.""" +def test_setQuerySettings_ns_coll_mismatch_accepted(collection): + """Test that indexHints ns.coll can differ from query shape collection.""" ctx = CommandContext.from_collection(collection) + query = { + "find": ctx.collection, + "filter": {"mis1": 1}, + "$db": ctx.database, + } try: - result = execute_admin_command(collection, test.build_command(ctx)) - assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) + result = execute_admin_command( + collection, + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": { + "db": ctx.database, + "coll": "completely_different_collection", + }, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + ) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="ns.coll mismatch should be accepted", + ) finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass + cleanup_query_settings(collection, [query]) # --------------------------------------------------------------------------- @@ -761,195 +746,88 @@ def test_setQuerySettings_field_verification(collection, test): # --------------------------------------------------------------------------- -# Group 7: No duplicate on update test +# Group 7: No duplicate on update test (standalone) # --------------------------------------------------------------------------- -SET_QUERY_SETTINGS_NO_DUPLICATE_TESTS: list[CommandTestCase] = [ - CommandTestCase( - "no_duplicate_on_update", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"dup1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - ], - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"dup1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": "classic", - }, - }, - expected={"count": 1}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"dup1": 1}, - "$db": ctx.database, - } - } - ], - msg="updating same shape should not create duplicate entries", - ), -] - @pytest.mark.admin @pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_NO_DUPLICATE_TESTS)) -def test_setQuerySettings_no_duplicate(collection, test): - """Test that updating same shape does not create duplicates.""" +def test_setQuerySettings_no_duplicate_on_update(collection): + """Test that updating same shape does not create duplicate entries.""" ctx = CommandContext.from_collection(collection) + query = { + "find": ctx.collection, + "filter": {"dup1": 1}, + "$db": ctx.database, + } + hints = { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + } try: - for cmd in test.build_setup(ctx): - execute_admin_command(collection, cmd) - r = execute_admin_command(collection, test.build_command(ctx)) + execute_admin_command( + collection, + {"setQuerySettings": query, "settings": hints}, + ) + r = execute_admin_command( + collection, + {"setQuerySettings": query, "settings": {**hints, "queryFramework": "classic"}}, + ) all_settings = get_query_settings(collection) count = sum(1 for s in all_settings if s.get("queryShapeHash") == r["queryShapeHash"]) assertSuccessPartial( {"count": count}, - test.build_expected(ctx), - msg=test.msg, + {"count": 1}, + msg="updating same shape should not create duplicate entries", ) finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass + cleanup_query_settings(collection, [query]) # --------------------------------------------------------------------------- -# Group 8: Multiple settings management tests +# Group 8: Multiple settings management test (standalone) # --------------------------------------------------------------------------- -SET_QUERY_SETTINGS_MULTI_TESTS: list[CommandTestCase] = [ - CommandTestCase( - "multiple_settings_all_visible", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"multi1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"multi2": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ], - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"multi3": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"all_present": True}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"multi1": 1}, - "$db": ctx.database, - } - }, - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"multi2": 1}, - "$db": ctx.database, - } - }, - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"multi3": 1}, - "$db": ctx.database, - } - }, - ], - msg="all 3 query settings should be visible in $querySettings", - ), -] - @pytest.mark.admin @pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_MULTI_TESTS)) -def test_setQuerySettings_multi(collection, test): +def test_setQuerySettings_multiple_settings_all_visible(collection): """Test that multiple query settings are independently visible.""" ctx = CommandContext.from_collection(collection) + queries = [ + {"find": ctx.collection, "filter": {"multi1": 1}, "$db": ctx.database}, + {"find": ctx.collection, "filter": {"multi2": 1}, "$db": ctx.database}, + {"find": ctx.collection, "filter": {"multi3": 1}, "$db": ctx.database}, + ] + hints = { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + } try: - setup_hashes = [] - for cmd in test.build_setup(ctx): - r = execute_admin_command(collection, cmd) - if "queryShapeHash" in r: - setup_hashes.append(r["queryShapeHash"]) - r = execute_admin_command(collection, test.build_command(ctx)) - setup_hashes.append(r["queryShapeHash"]) + hashes = [] + for q in queries: + r = execute_admin_command( + collection, + {"setQuerySettings": q, "settings": hints}, + ) + hashes.append(r["queryShapeHash"]) all_hashes = {s.get("queryShapeHash") for s in get_query_settings(collection)} - all_present = all(h in all_hashes for h in setup_hashes) + all_present = all(h in all_hashes for h in hashes) assertSuccessPartial( {"all_present": all_present}, - test.build_expected(ctx), - msg=test.msg, + {"all_present": True}, + msg="all 3 query settings should be visible in $querySettings", ) finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass + cleanup_query_settings(collection, queries) # --------------------------------------------------------------------------- From 39f64d6010e0be1096ab93e07f28e658cb5a5027 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 17:08:06 -0700 Subject: [PATCH 15/27] convert one standalone Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_verification.py | 75 +++++++++++-------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py index 073f82b10..64ce58346 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py +++ b/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py @@ -41,45 +41,60 @@ # --------------------------------------------------------------------------- -# Group 1: ns.coll mismatch acceptance test (standalone) +# Group 1: ns.coll mismatch acceptance test # --------------------------------------------------------------------------- +SET_QUERY_SETTINGS_NS_MISMATCH_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "ns_coll_mismatch_accepted", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"mis1": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": { + "db": ctx.database, + "coll": "completely_different_collection", + }, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"mis1": 1}, + "$db": ctx.database, + } + } + ], + msg="ns.coll mismatch should be accepted", + ), +] + @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_ns_coll_mismatch_accepted(collection): +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_NS_MISMATCH_TESTS)) +def test_setQuerySettings_ns_coll_mismatch_accepted(collection, test): """Test that indexHints ns.coll can differ from query shape collection.""" ctx = CommandContext.from_collection(collection) - query = { - "find": ctx.collection, - "filter": {"mis1": 1}, - "$db": ctx.database, - } try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": { - "db": ctx.database, - "coll": "completely_different_collection", - }, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="ns.coll mismatch should be accepted", - ) + result = execute_admin_command(collection, test.build_command(ctx)) + assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass # --------------------------------------------------------------------------- From 5205d24eed806fe9411a1e0d51382791f808d499 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 17:41:48 -0700 Subject: [PATCH 16/27] rename to query_planning directory Signed-off-by: Alina (Xi) Li --- .../__init__.py | 0 .../commands}/__init__.py | 0 .../commands/planCacheClear}/__init__.py | 0 .../test_planCacheClear_behavior.py | 0 ...est_planCacheClear_collation_collection.py | 0 .../test_planCacheClear_collection_errors.py | 0 .../test_planCacheClear_core.py | 0 .../test_planCacheClear_dependencies.py | 0 .../test_planCacheClear_field_type.py | 0 .../test_planCacheClear_query_comment_type.py | 0 ...est_planCacheClear_sort_projection_type.py | 0 .../test_smoke_planCacheClear.py | 0 .../test_smoke_planCacheClearFilters.py | 0 .../test_smoke_planCacheListFilters.py | 0 .../test_smoke_planCacheSetFilter.py | 0 .../test_smoke_removeQuerySettings.py | 0 .../commands/setQuerySettings/__init__.py | 0 .../test_setQuerySettings_behavior.py | 16 +++--- .../test_setQuerySettings_query_shapes.py | 42 +++++++------- .../test_setQuerySettings_reject.py | 20 ++++--- .../test_setQuerySettings_settings.py | 48 ++++++++-------- .../test_setQuerySettings_type_errors.py | 0 ...test_setQuerySettings_validation_errors.py | 0 .../test_setQuerySettings_verification.py | 40 ++++++------- .../test_smoke_setQuerySettings.py | 0 .../setQuerySettings/utils/__init__.py | 0 .../utils/setQuerySettings_common.py | 0 .../core/query_planning/utils/__init__.py | 0 .../utils/settings_test_case.py | 57 +++++++++++++++++++ .../tests/core/utils/command_test_case.py | 22 ------- 30 files changed, 145 insertions(+), 100 deletions(-) rename documentdb_tests/compatibility/tests/core/{query-planning/commands/planCacheClear => query_planning}/__init__.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning/commands/setQuerySettings => query_planning/commands}/__init__.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning/commands/setQuerySettings/utils => query_planning/commands/planCacheClear}/__init__.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheClear/test_planCacheClear_behavior.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheClear/test_planCacheClear_collation_collection.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheClear/test_planCacheClear_collection_errors.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheClear/test_planCacheClear_core.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheClear/test_planCacheClear_dependencies.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheClear/test_planCacheClear_field_type.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheClear/test_planCacheClear_query_comment_type.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheClear/test_planCacheClear_sort_projection_type.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheClear/test_smoke_planCacheClear.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheClearFilters/test_smoke_planCacheClearFilters.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheListFilters/test_smoke_planCacheListFilters.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/planCacheSetFilter/test_smoke_planCacheSetFilter.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/removeQuerySettings/test_smoke_removeQuerySettings.py (100%) create mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/__init__.py rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/setQuerySettings/test_setQuerySettings_behavior.py (97%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/setQuerySettings/test_setQuerySettings_query_shapes.py (96%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/setQuerySettings/test_setQuerySettings_reject.py (95%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/setQuerySettings/test_setQuerySettings_settings.py (97%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/setQuerySettings/test_setQuerySettings_type_errors.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/setQuerySettings/test_setQuerySettings_validation_errors.py (100%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/setQuerySettings/test_setQuerySettings_verification.py (97%) rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/setQuerySettings/test_smoke_setQuerySettings.py (100%) create mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/__init__.py rename documentdb_tests/compatibility/tests/core/{query-planning => query_planning}/commands/setQuerySettings/utils/setQuerySettings_common.py (100%) create mode 100644 documentdb_tests/compatibility/tests/core/query_planning/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/query_planning/utils/settings_test_case.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/__init__.py b/documentdb_tests/compatibility/tests/core/query_planning/__init__.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/__init__.py rename to documentdb_tests/compatibility/tests/core/query_planning/__init__.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/__init__.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/__init__.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/__init__.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/__init__.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/utils/__init__.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/__init__.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/utils/__init__.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/__init__.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_behavior.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_behavior.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_behavior.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_behavior.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_collation_collection.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_collation_collection.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_collation_collection.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_collation_collection.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_collection_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_collection_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_collection_errors.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_collection_errors.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_core.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_core.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_core.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_core.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_dependencies.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_dependencies.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_dependencies.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_dependencies.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_field_type.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_field_type.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_field_type.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_field_type.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_query_comment_type.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_query_comment_type.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_query_comment_type.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_query_comment_type.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_sort_projection_type.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_sort_projection_type.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_planCacheClear_sort_projection_type.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_planCacheClear_sort_projection_type.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_smoke_planCacheClear.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_smoke_planCacheClear.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClear/test_smoke_planCacheClear.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClear/test_smoke_planCacheClear.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClearFilters/test_smoke_planCacheClearFilters.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClearFilters/test_smoke_planCacheClearFilters.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheClearFilters/test_smoke_planCacheClearFilters.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheClearFilters/test_smoke_planCacheClearFilters.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheListFilters/test_smoke_planCacheListFilters.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheListFilters/test_smoke_planCacheListFilters.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheListFilters/test_smoke_planCacheListFilters.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheListFilters/test_smoke_planCacheListFilters.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheSetFilter/test_smoke_planCacheSetFilter.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheSetFilter/test_smoke_planCacheSetFilter.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/planCacheSetFilter/test_smoke_planCacheSetFilter.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/planCacheSetFilter/test_smoke_planCacheSetFilter.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/removeQuerySettings/test_smoke_removeQuerySettings.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_smoke_removeQuerySettings.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/removeQuerySettings/test_smoke_removeQuerySettings.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_smoke_removeQuerySettings.py diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/__init__.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py similarity index 97% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py index c5e62c023..c6e9ea0e8 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_behavior.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py @@ -10,9 +10,11 @@ import pytest from pymongo.collection import Collection +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, - CommandTestCase, ) from documentdb_tests.framework.assertions import assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command @@ -21,8 +23,8 @@ from .utils.setQuerySettings_common import cleanup_query_settings, get_query_settings # Property [Response Structure]: setQuerySettings response includes hash, query, and settings. -SET_QUERY_SETTINGS_RESPONSE_TESTS: list[CommandTestCase] = [ - CommandTestCase( +SET_QUERY_SETTINGS_RESPONSE_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( "response_contains_hash", command=lambda ctx: { "setQuerySettings": { @@ -51,7 +53,7 @@ ], msg="response should contain queryShapeHash", ), - CommandTestCase( + SettingsTestCase( "response_contains_representative_query", command=lambda ctx: { "setQuerySettings": { @@ -80,7 +82,7 @@ ], msg="response should contain representativeQuery", ), - CommandTestCase( + SettingsTestCase( "response_settings_echo", command=lambda ctx: { "setQuerySettings": { @@ -146,8 +148,8 @@ def test_setQuerySettings_response(collection, test): # Property [removeQuerySettings]: settings can be removed by query or hash. -SET_QUERY_SETTINGS_REMOVE_TESTS: list[CommandTestCase] = [ - CommandTestCase( +SET_QUERY_SETTINGS_REMOVE_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( "removeQuerySettings_by_query", setup_commands=lambda ctx: [ { diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py similarity index 96% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py index ec9bd0469..9a975aef8 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py @@ -9,9 +9,11 @@ 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, - CommandTestCase, ) from documentdb_tests.framework.assertions import assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command @@ -22,9 +24,9 @@ # Property [Distinct Shape Variations]: setQuerySettings accepts distinct shapes with query combos. # Property [Aggregate Shape Variations]: setQuerySettings accepts aggregate pipeline shapes. # Property [$db Field Variations]: setQuerySettings accepts non-existent and special-char db names. -SET_QUERY_SETTINGS_QUERY_SHAPE_TESTS: list[CommandTestCase] = [ +SET_QUERY_SETTINGS_QUERY_SHAPE_TESTS: list[SettingsTestCase] = [ # -- Command shape acceptance -- - CommandTestCase( + SettingsTestCase( "find_shape", command=lambda ctx: { "setQuerySettings": { @@ -55,7 +57,7 @@ ], msg="should accept valid find shape", ), - CommandTestCase( + SettingsTestCase( "distinct_shape", command=lambda ctx: { "setQuerySettings": { @@ -86,7 +88,7 @@ ], msg="should accept valid distinct shape", ), - CommandTestCase( + SettingsTestCase( "aggregate_shape", command=lambda ctx: { "setQuerySettings": { @@ -116,7 +118,7 @@ msg="should accept valid aggregate shape", ), # -- Find shape variations -- - CommandTestCase( + SettingsTestCase( "find_filter_only", command=lambda ctx: { "setQuerySettings": { @@ -145,7 +147,7 @@ ], msg="should accept find with filter only", ), - CommandTestCase( + SettingsTestCase( "find_filter_sort", command=lambda ctx: { "setQuerySettings": { @@ -176,7 +178,7 @@ ], msg="should accept find with filter+sort", ), - CommandTestCase( + SettingsTestCase( "find_filter_projection", command=lambda ctx: { "setQuerySettings": { @@ -207,7 +209,7 @@ ], msg="should accept find with filter+projection", ), - CommandTestCase( + SettingsTestCase( "find_filter_sort_projection", command=lambda ctx: { "setQuerySettings": { @@ -240,7 +242,7 @@ ], msg="should accept find with all fields", ), - CommandTestCase( + SettingsTestCase( "find_with_collation", command=lambda ctx: { "setQuerySettings": { @@ -271,7 +273,7 @@ ], msg="should accept find with collation", ), - CommandTestCase( + SettingsTestCase( "find_with_let", command=lambda ctx: { "setQuerySettings": { @@ -302,7 +304,7 @@ ], msg="should accept find with let", ), - CommandTestCase( + SettingsTestCase( "find_with_limit", command=lambda ctx: { "setQuerySettings": { @@ -334,7 +336,7 @@ msg="should accept find with limit", ), # -- Distinct shape variations -- - CommandTestCase( + SettingsTestCase( "distinct_key_only", command=lambda ctx: { "setQuerySettings": { @@ -363,7 +365,7 @@ ], msg="should accept distinct key only", ), - CommandTestCase( + SettingsTestCase( "distinct_complex_query", command=lambda ctx: { "setQuerySettings": { @@ -395,7 +397,7 @@ msg="should accept distinct complex query", ), # -- Aggregate shape variations -- - CommandTestCase( + SettingsTestCase( "aggregate_match_only", command=lambda ctx: { "setQuerySettings": { @@ -424,7 +426,7 @@ ], msg="should accept aggregate $match only", ), - CommandTestCase( + SettingsTestCase( "aggregate_match_group", command=lambda ctx: { "setQuerySettings": { @@ -459,7 +461,7 @@ ], msg="should accept aggregate $match+$group", ), - CommandTestCase( + SettingsTestCase( "aggregate_match_sort_limit", command=lambda ctx: { "setQuerySettings": { @@ -488,7 +490,7 @@ ], msg="should accept aggregate $match+$sort+$limit", ), - CommandTestCase( + SettingsTestCase( "aggregate_empty_pipeline", command=lambda ctx: { "setQuerySettings": { @@ -518,7 +520,7 @@ msg="should accept aggregate with empty pipeline", ), # -- $db field variations -- - CommandTestCase( + SettingsTestCase( "db_nonexistent", command=lambda ctx: { "setQuerySettings": { @@ -550,7 +552,7 @@ ], msg="should accept non-existent $db", ), - CommandTestCase( + SettingsTestCase( "db_special_characters", command=lambda ctx: { "setQuerySettings": { diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py similarity index 95% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py index 6decf6ddd..a05d831c9 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_reject.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py @@ -9,9 +9,11 @@ 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, - CommandTestCase, ) from documentdb_tests.framework.assertions import assertResult, assertSuccessPartial from documentdb_tests.framework.error_codes import QUERYSETTINGS_QUERY_REJECTED_ERROR @@ -25,8 +27,8 @@ # Property [Reject Reversal via Remove]: removing the query setting re-enables the query. # Property [Reject False Succeeds]: reject: false with indexHints allows the query. -SET_QUERY_SETTINGS_REJECT_ERROR_TESTS: list[CommandTestCase] = [ - CommandTestCase( +SET_QUERY_SETTINGS_REJECT_ERROR_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( "reject_blocks_distinct", setup_commands=lambda ctx: [ { @@ -57,7 +59,7 @@ ], msg="distinct query matching reject: true should be rejected", ), - CommandTestCase( + SettingsTestCase( "reject_blocks_aggregate", setup_commands=lambda ctx: [ { @@ -89,8 +91,8 @@ ] -SET_QUERY_SETTINGS_REJECT_SUCCESS_TESTS: list[CommandTestCase] = [ - CommandTestCase( +SET_QUERY_SETTINGS_REJECT_SUCCESS_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( "reject_does_not_affect_different_shape", setup_commands=lambda ctx: [ { @@ -118,7 +120,7 @@ ], msg="different query shape should not be rejected", ), - CommandTestCase( + SettingsTestCase( "reject_reversed_by_update", setup_commands=lambda ctx: [ { @@ -162,7 +164,7 @@ ], msg="query should succeed after reject updated to false", ), - CommandTestCase( + SettingsTestCase( "reject_reversed_by_remove", setup_commands=lambda ctx: [ { @@ -197,7 +199,7 @@ ], msg="query should succeed after removeQuerySettings", ), - CommandTestCase( + SettingsTestCase( "reject_false_allows_query", setup_commands=lambda ctx: [ { diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py similarity index 97% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py index 463458aa3..d4e23c61d 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_settings.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py @@ -9,9 +9,11 @@ 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, - CommandTestCase, ) from documentdb_tests.framework.assertions import assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command @@ -31,8 +33,8 @@ # Property [2dsphere Index Spec]: setQuerySettings accepts 2dsphere index key pattern. # Property [2d Index Spec]: setQuerySettings accepts 2d index key pattern. # Property [Hashed Index Spec]: setQuerySettings accepts hashed index key pattern. -SET_QUERY_SETTINGS_SETTINGS_TESTS: list[CommandTestCase] = [ - CommandTestCase( +SET_QUERY_SETTINGS_SETTINGS_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( "indexHints_single_index", command=lambda ctx: { "setQuerySettings": { @@ -61,7 +63,7 @@ ], msg="should accept indexHints with single index", ), - CommandTestCase( + SettingsTestCase( "indexHints_multiple_indexes", command=lambda ctx: { "setQuerySettings": { @@ -90,7 +92,7 @@ ], msg="should accept multiple indexes", ), - CommandTestCase( + SettingsTestCase( "indexHints_key_pattern", command=lambda ctx: { "setQuerySettings": { @@ -119,7 +121,7 @@ ], msg="should accept indexHints with key pattern", ), - CommandTestCase( + SettingsTestCase( "reject_true", command=lambda ctx: { "setQuerySettings": { @@ -141,7 +143,7 @@ ], msg="should accept settings with reject: true", ), - CommandTestCase( + SettingsTestCase( "reject_with_indexHints", command=lambda ctx: { "setQuerySettings": { @@ -171,7 +173,7 @@ ], msg="should accept reject with indexHints", ), - CommandTestCase( + SettingsTestCase( "queryFramework_classic", command=lambda ctx: { "setQuerySettings": { @@ -201,7 +203,7 @@ ], msg="should accept queryFramework: classic", ), - CommandTestCase( + SettingsTestCase( "queryFramework_sbe", command=lambda ctx: { "setQuerySettings": { @@ -231,7 +233,7 @@ ], msg="should accept queryFramework: sbe", ), - CommandTestCase( + SettingsTestCase( "with_comment_string", command=lambda ctx: { "setQuerySettings": { @@ -261,7 +263,7 @@ ], msg="should accept settings with comment string", ), - CommandTestCase( + SettingsTestCase( "all_settings_combined", command=lambda ctx: { "setQuerySettings": { @@ -292,7 +294,7 @@ ], msg="should accept all settings combined", ), - CommandTestCase( + SettingsTestCase( "indexHints_natural", command=lambda ctx: { "setQuerySettings": { @@ -321,7 +323,7 @@ ], msg="should accept $natural in allowedIndexes", ), - CommandTestCase( + SettingsTestCase( "indexHints_multiple_ns_documents", command=lambda ctx: { "setQuerySettings": { @@ -354,7 +356,7 @@ ], msg="should accept multiple indexHints documents", ), - CommandTestCase( + SettingsTestCase( "indexHints_nonexistent_index", command=lambda ctx: { "setQuerySettings": { @@ -383,7 +385,7 @@ ], msg="should accept non-existent index name", ), - CommandTestCase( + SettingsTestCase( "comment_object", command=lambda ctx: { "setQuerySettings": { @@ -413,7 +415,7 @@ ], msg="should accept settings with comment as object", ), - CommandTestCase( + SettingsTestCase( "comment_int", command=lambda ctx: { "setQuerySettings": { @@ -443,7 +445,7 @@ ], msg="should accept settings with comment as int", ), - CommandTestCase( + SettingsTestCase( "comment_bool", command=lambda ctx: { "setQuerySettings": { @@ -473,7 +475,7 @@ ], msg="should accept settings with comment as bool", ), - CommandTestCase( + SettingsTestCase( "comment_array", command=lambda ctx: { "setQuerySettings": { @@ -503,7 +505,7 @@ ], msg="should accept settings with comment as array", ), - CommandTestCase( + SettingsTestCase( "comment_null", command=lambda ctx: { "setQuerySettings": { @@ -533,7 +535,7 @@ ], msg="should accept settings with comment as null", ), - CommandTestCase( + SettingsTestCase( "indexHints_text_index_spec", command=lambda ctx: { "setQuerySettings": { @@ -562,7 +564,7 @@ ], msg="should accept text index key pattern in allowedIndexes", ), - CommandTestCase( + SettingsTestCase( "indexHints_2dsphere_index_spec", command=lambda ctx: { "setQuerySettings": { @@ -591,7 +593,7 @@ ], msg="should accept 2dsphere index key pattern in allowedIndexes", ), - CommandTestCase( + SettingsTestCase( "indexHints_2d_index_spec", command=lambda ctx: { "setQuerySettings": { @@ -620,7 +622,7 @@ ], msg="should accept 2d index key pattern in allowedIndexes", ), - CommandTestCase( + SettingsTestCase( "indexHints_hashed_index_spec", command=lambda ctx: { "setQuerySettings": { diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py similarity index 97% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py index 64ce58346..de82b5544 100644 --- a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_setQuerySettings_verification.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py @@ -12,9 +12,11 @@ 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, - CommandTestCase, ) from documentdb_tests.framework.assertions import assertSuccessPartial from documentdb_tests.framework.executor import execute_admin_command @@ -44,8 +46,8 @@ # Group 1: ns.coll mismatch acceptance test # --------------------------------------------------------------------------- -SET_QUERY_SETTINGS_NS_MISMATCH_TESTS: list[CommandTestCase] = [ - CommandTestCase( +SET_QUERY_SETTINGS_NS_MISMATCH_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( "ns_coll_mismatch_accepted", command=lambda ctx: { "setQuerySettings": { @@ -101,8 +103,8 @@ def test_setQuerySettings_ns_coll_mismatch_accepted(collection, test): # Group 2: Hash property tests # --------------------------------------------------------------------------- -SET_QUERY_SETTINGS_HASH_SAME_TESTS: list[CommandTestCase] = [ - CommandTestCase( +SET_QUERY_SETTINGS_HASH_SAME_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( "same_shape_produces_same_hash", setup_commands=lambda ctx: [ { @@ -147,7 +149,7 @@ def test_setQuerySettings_ns_coll_mismatch_accepted(collection, test): ], msg="same query shape should produce identical hashes", ), - CommandTestCase( + SettingsTestCase( "filter_values_do_not_affect_shape", setup_commands=lambda ctx: [ { @@ -201,8 +203,8 @@ def test_setQuerySettings_ns_coll_mismatch_accepted(collection, test): ), ] -SET_QUERY_SETTINGS_HASH_DIFFERENT_TESTS: list[CommandTestCase] = [ - CommandTestCase( +SET_QUERY_SETTINGS_HASH_DIFFERENT_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( "different_shapes_different_hashes", setup_commands=lambda ctx: [ { @@ -254,7 +256,7 @@ def test_setQuerySettings_ns_coll_mismatch_accepted(collection, test): ], msg="different query shapes should produce different hashes", ), - CommandTestCase( + SettingsTestCase( "sort_direction_affects_shape", setup_commands=lambda ctx: [ { @@ -411,8 +413,8 @@ def test_setQuerySettings_hash_is_64_char_hex(collection): # Group 4: $querySettings inspection tests # --------------------------------------------------------------------------- -SET_QUERY_SETTINGS_QS_STAGE_TESTS: list[CommandTestCase] = [ - CommandTestCase( +SET_QUERY_SETTINGS_QS_STAGE_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( "querySettings_returns_distinct_shape", command=lambda ctx: { "setQuerySettings": { @@ -443,7 +445,7 @@ def test_setQuerySettings_hash_is_64_char_hex(collection): ], msg="representativeQuery should be a distinct shape", ), - CommandTestCase( + SettingsTestCase( "querySettings_returns_aggregate_shape", command=lambda ctx: { "setQuerySettings": { @@ -503,8 +505,8 @@ def test_setQuerySettings_qs_stage(collection, test): # Group 5: showDebugQueryShape tests # --------------------------------------------------------------------------- -SET_QUERY_SETTINGS_DEBUG_SHAPE_TESTS: list[CommandTestCase] = [ - CommandTestCase( +SET_QUERY_SETTINGS_DEBUG_SHAPE_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( "debug_query_shape_present_when_enabled", command=lambda ctx: { "setQuerySettings": { @@ -533,7 +535,7 @@ def test_setQuerySettings_qs_stage(collection, test): ], msg="debugQueryShape should be present with showDebugQueryShape: true", ), - CommandTestCase( + SettingsTestCase( "debug_query_shape_absent_when_disabled", command=lambda ctx: { "setQuerySettings": { @@ -605,8 +607,8 @@ def test_setQuerySettings_debug_shape(collection, test): # (comment visibility, comment update, settings replacement) # --------------------------------------------------------------------------- -SET_QUERY_SETTINGS_FIELD_VERIFICATION_TESTS: list[CommandTestCase] = [ - CommandTestCase( +SET_QUERY_SETTINGS_FIELD_VERIFICATION_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( "comment_visible_in_querySettings", command=lambda ctx: { "setQuerySettings": { @@ -636,7 +638,7 @@ def test_setQuerySettings_debug_shape(collection, test): ], msg="comment should be visible in $querySettings output", ), - CommandTestCase( + SettingsTestCase( "comment_replaced_on_update", setup_commands=lambda ctx: [ { @@ -684,7 +686,7 @@ def test_setQuerySettings_debug_shape(collection, test): ], msg="comment should be replaced by the updated value", ), - CommandTestCase( + SettingsTestCase( "update_preserves_unmodified_fields", setup_commands=lambda ctx: [ { diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_smoke_setQuerySettings.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_smoke_setQuerySettings.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/test_smoke_setQuerySettings.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_smoke_setQuerySettings.py diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/__init__.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/utils/setQuerySettings_common.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/setQuerySettings_common.py similarity index 100% rename from documentdb_tests/compatibility/tests/core/query-planning/commands/setQuerySettings/utils/setQuerySettings_common.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/setQuerySettings_common.py diff --git a/documentdb_tests/compatibility/tests/core/query_planning/utils/__init__.py b/documentdb_tests/compatibility/tests/core/query_planning/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/query_planning/utils/settings_test_case.py b/documentdb_tests/compatibility/tests/core/query_planning/utils/settings_test_case.py new file mode 100644 index 000000000..64eef24a5 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_planning/utils/settings_test_case.py @@ -0,0 +1,57 @@ +"""Test case with setup/cleanup lifecycle for settings-based commands. + +``SettingsTestCase`` extends ``CommandTestCase`` with ``setup_commands`` +and ``cleanup`` hooks for commands that require prerequisite operations +(e.g. creating a query setting before testing removal) and post-test +teardown (e.g. removing cluster-wide query settings). + +Results returned by each setup command are appended to +``setup_results`` so that later lambdas (``command``, ``expected``, +etc.) can reference values produced during setup. +""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Any + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) + + +@dataclass(frozen=True) +class SettingsTestCase(CommandTestCase): + """CommandTestCase with setup-command and cleanup lifecycle. + + Attributes: + setup_commands: Optional callable ``(CommandContext) -> list[dict]`` + returning commands to execute **before** the main command. + Each command's result is appended to ``setup_results`` + by the runner. + cleanup: Optional callable ``(CommandContext) -> list[dict]`` + returning commands to run after the test. Each dict is + passed to the executor inside a try/except so cleanup + failures are silently ignored. + setup_results: Results from setup commands, populated by the + runner. Mutable even in a frozen dataclass so runners can + append after construction. + """ + + setup_commands: Callable[[CommandContext], list[dict[str, Any]]] | None = None + cleanup: Callable[[CommandContext], list[dict[str, Any]]] | None = None + setup_results: list[dict[str, Any]] = field(default_factory=list) + + def build_setup(self, ctx: CommandContext) -> list[dict[str, Any]]: + """Resolve setup commands from the callable, or return empty list.""" + if self.setup_commands is None: + return [] + return self.setup_commands(ctx) + + def build_cleanup(self, ctx: CommandContext) -> list[dict[str, Any]]: + """Resolve cleanup commands from the callable, or return empty list.""" + if self.cleanup is None: + return [] + return self.cleanup(ctx) diff --git a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py index e5fc9fc12..121027368 100644 --- a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py +++ b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py @@ -69,14 +69,6 @@ class CommandTestCase(BaseTestCase): for error cases. ignore_order_in: Optional names of result fields whose array contents should be compared without regard to element order. - setup_commands: Optional callable ``(CommandContext) -> list[dict]`` - returning commands to execute **before** the main command. - Use for prerequisite operations such as creating a query - setting before testing removal. - cleanup: Optional callable ``(CommandContext) -> list[dict]`` - returning commands to run after the test. Each dict is - passed to the executor inside a try/except so cleanup - failures are silently ignored. """ target_collection: TargetCollection = field(default_factory=TargetCollection) @@ -86,8 +78,6 @@ class CommandTestCase(BaseTestCase): command: dict[str, Any] | Callable[..., dict[str, Any]] | None = None expected: dict[str, Any] | list[dict[str, Any]] | Callable[..., dict[str, Any]] | None = None ignore_order_in: list[str] | None = None - setup_commands: Callable[[CommandContext], list[dict[str, Any]]] | None = None - cleanup: Callable[[CommandContext], list[dict[str, Any]]] | None = None def prepare(self, db: Database, collection: Collection) -> Collection: """Resolve the target collection and apply indexes/docs. @@ -128,15 +118,3 @@ def build_expected(self, ctx: CommandContext) -> dict[str, Any] | list[dict[str, if self.expected is None or isinstance(self.expected, (dict, list)): return self.expected return self.expected(ctx) - - def build_setup(self, ctx: CommandContext) -> list[dict[str, Any]]: - """Resolve setup commands from the callable, or return empty list.""" - if self.setup_commands is None: - return [] - return self.setup_commands(ctx) - - def build_cleanup(self, ctx: CommandContext) -> list[dict[str, Any]]: - """Resolve cleanup commands from the callable, or return empty list.""" - if self.cleanup is None: - return [] - return self.cleanup(ctx) From 8e67e62a2a605d5fcbcebc5c6cfa1d35db1debb9 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 17:56:32 -0700 Subject: [PATCH 17/27] convert to use SettingsTestCase Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_behavior.py | 258 ++++++------- .../test_setQuerySettings_reject.py | 34 +- .../test_setQuerySettings_settings.py | 152 ++++---- ...test_setQuerySettings_validation_errors.py | 41 +- .../test_setQuerySettings_verification.py | 350 +++++++++++------- .../utils/settings_test_case.py | 10 +- .../tests/core/utils/command_test_case.py | 4 + 7 files changed, 472 insertions(+), 377 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py index c6e9ea0e8..c3353a7c8 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py @@ -8,7 +8,6 @@ from __future__ import annotations import pytest -from pymongo.collection import Collection from documentdb_tests.compatibility.tests.core.query_planning.utils.settings_test_case import ( SettingsTestCase, @@ -20,7 +19,7 @@ from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params -from .utils.setQuerySettings_common import cleanup_query_settings, get_query_settings +from .utils.setQuerySettings_common import get_query_settings # Property [Response Structure]: setQuerySettings response includes hash, query, and settings. SET_QUERY_SETTINGS_RESPONSE_TESTS: list[SettingsTestCase] = [ @@ -187,6 +186,38 @@ def test_setQuerySettings_response(collection, test): ], msg="removeQuerySettings by query should succeed", ), + SettingsTestCase( + "removeQuerySettings_by_hash", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"b6": 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": {"b6": 1}, + "$db": ctx.database, + } + } + ], + msg="removeQuerySettings by hash should succeed", + ), ] @@ -198,7 +229,8 @@ def test_setQuerySettings_remove(collection, test): ctx = CommandContext.from_collection(collection) try: for cmd in test.build_setup(ctx): - execute_admin_command(collection, cmd) + 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: @@ -209,160 +241,136 @@ def test_setQuerySettings_remove(collection, test): pass -# Property [removeQuerySettings by hash]: requires capturing hash from setup result. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_removeQuerySettings_by_hash(collection: Collection): - """Test removeQuerySettings removes settings by query shape hash.""" - query = { - "find": collection.name, - "filter": {"b6": 1}, - "$db": collection.database.name, - } - try: - setup_result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - - query_hash = setup_result.get("queryShapeHash") - result = execute_admin_command( - collection, - {"removeQuerySettings": query_hash}, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="removeQuerySettings by hash should succeed") - finally: - cleanup_query_settings(collection, [query]) - - # Property [$querySettings Retrieval]: settings are visible via $querySettings aggregation stage. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_querySettings_stage_retrieval(collection: Collection): - """Test query settings are visible via $querySettings aggregation stage.""" - query = { - "find": collection.name, - "filter": {"b4": 1}, - "$db": collection.database.name, - } - try: - setup_result = execute_admin_command( - collection, +SET_QUERY_SETTINGS_QS_STAGE_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( + "querySettings_stage_retrieval", + setup_commands=lambda ctx: [ { - "setQuerySettings": query, + "setQuerySettings": { + "find": ctx.collection, + "filter": {"b4": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { - "ns": {"db": collection.database.name, "coll": collection.name}, + "ns": {"db": ctx.database, "coll": ctx.collection}, "allowedIndexes": ["_id_"], } ], }, - }, - ) - expected_hash = setup_result.get("queryShapeHash") - - settings = get_query_settings(collection) - matching = [s for s in settings if s.get("queryShapeHash") == expected_hash] - assertSuccessPartial( - matching[0] if matching else {}, - {"queryShapeHash": expected_hash}, - msg="$querySettings should return the created setting", - ) - finally: - cleanup_query_settings(collection, [query]) - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_querySettings_stage_shows_settings(collection: Collection): - """Test $querySettings stage includes indexHints in the returned settings.""" - query = { - "find": collection.name, - "filter": {"b9": 1}, - "$db": collection.database.name, - } - try: - setup_result = execute_admin_command( - collection, + } + ], + expected=lambda ctx: {"queryShapeHash": ctx.setup_results[0]["queryShapeHash"]}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b4": 1}, + "$db": ctx.database, + } + } + ], + msg="$querySettings should return the created setting", + ), + SettingsTestCase( + "querySettings_stage_shows_settings", + setup_commands=lambda ctx: [ { - "setQuerySettings": query, + "setQuerySettings": { + "find": ctx.collection, + "filter": {"b9": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { - "ns": {"db": collection.database.name, "coll": collection.name}, + "ns": {"db": ctx.database, "coll": ctx.collection}, "allowedIndexes": ["_id_"], } ], }, + } + ], + expected=lambda ctx: { + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], }, - ) - expected_hash = setup_result.get("queryShapeHash") - - settings = get_query_settings(collection) - matching = [s for s in settings if s.get("queryShapeHash") == expected_hash] - entry = matching[0] if matching else {} - assertSuccessPartial( - entry, + }, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b9": 1}, + "$db": ctx.database, + } + } + ], + msg="$querySettings should include indexHints in settings", + ), + SettingsTestCase( + "querySettings_stage_shows_representative_query", + setup_commands=lambda ctx: [ { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"b10": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { - "ns": {"db": collection.database.name, "coll": collection.name}, + "ns": {"db": ctx.database, "coll": ctx.collection}, "allowedIndexes": ["_id_"], } ], }, - }, - msg="$querySettings should include indexHints in settings", - ) - finally: - cleanup_query_settings(collection, [query]) + } + ], + # expected is built dynamically in the runner (self-referential) + expected=None, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b10": 1}, + "$db": ctx.database, + } + } + ], + msg="$querySettings should include representativeQuery", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_querySettings_stage_shows_representative_query(collection: Collection): - """Test $querySettings stage includes representativeQuery in the output.""" - query = { - "find": collection.name, - "filter": {"b10": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_QS_STAGE_TESTS)) +def test_setQuerySettings_qs_stage(collection, test): + """Test that settings are visible via $querySettings aggregation stage.""" + ctx = CommandContext.from_collection(collection) try: - setup_result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": collection.database.name, "coll": collection.name}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - expected_hash = setup_result.get("queryShapeHash") - + 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"] settings = get_query_settings(collection) matching = [s for s in settings if s.get("queryShapeHash") == expected_hash] entry = matching[0] if matching else {} - assertSuccessPartial( - entry, - {"representativeQuery": entry.get("representativeQuery")}, - msg="$querySettings should include representativeQuery", - ) + expected = test.build_expected(ctx) + if expected is None: + # Self-referential: verify the field exists + expected = {"representativeQuery": entry.get("representativeQuery")} + assertSuccessPartial(entry, expected, msg=test.msg) finally: - cleanup_query_settings(collection, [query]) + 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/setQuerySettings/test_setQuerySettings_reject.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py index a05d831c9..a47c0f318 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py @@ -28,6 +28,34 @@ # Property [Reject False Succeeds]: reject: false with indexHints allows the query. SET_QUERY_SETTINGS_REJECT_ERROR_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( + "reject_blocks_find", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"b8": 1}, + "$db": ctx.database, + }, + "settings": {"reject": True}, + } + ], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"b8": 1}, + }, + error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b8": 1}, + "$db": ctx.database, + } + } + ], + msg="query matching reject: true setting should be rejected", + ), SettingsTestCase( "reject_blocks_distinct", setup_commands=lambda ctx: [ @@ -246,7 +274,8 @@ def test_setQuerySettings_reject_errors(collection, test): ctx = CommandContext.from_collection(collection) try: for cmd in test.build_setup(ctx): - execute_admin_command(collection, cmd) + r = execute_admin_command(collection, cmd) + ctx.setup_results.append(r) result = execute_command(collection, test.build_command(ctx)) assertResult(result, error_code=test.error_code, msg=test.msg) finally: @@ -265,7 +294,8 @@ def test_setQuerySettings_reject_success(collection, test): ctx = CommandContext.from_collection(collection) try: for cmd in test.build_setup(ctx): - execute_admin_command(collection, cmd) + r = execute_admin_command(collection, cmd) + ctx.setup_results.append(r) result = execute_command(collection, test.build_command(ctx)) assertSuccessPartial(result, test.build_expected(ctx), msg=test.msg) finally: diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py index d4e23c61d..f5c46ff44 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py @@ -19,8 +19,6 @@ from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params -from .utils.setQuerySettings_common import cleanup_query_settings - # Property [indexHints Acceptance]: setQuerySettings accepts valid indexHints configurations. # Property [reject Acceptance]: setQuerySettings accepts reject: true alone or with indexHints. # Property [queryFramework Acceptance]: setQuerySettings accepts classic and sbe frameworks. @@ -661,6 +659,9 @@ def test_setQuerySettings_settings(collection, test): """Test setQuerySettings accepts valid settings configurations.""" 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: @@ -672,21 +673,16 @@ def test_setQuerySettings_settings(collection, test): # Property [Update Behavior]: setQuerySettings can update existing settings by query or hash. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_update_existing_settings(collection): - """Test setQuerySettings can update settings for an existing query shape.""" - ctx = CommandContext.from_collection(collection) - query = { - "find": ctx.collection, - "filter": {"a10": 1}, - "$db": ctx.database, - } - try: - execute_admin_command( - collection, +SET_QUERY_SETTINGS_UPDATE_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( + "update_existing_settings", + setup_commands=lambda ctx: [ { - "setQuerySettings": query, + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a10": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { @@ -695,69 +691,95 @@ def test_setQuerySettings_update_existing_settings(collection): } ], }, + } + ], + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a10": 1}, + "$db": ctx.database, }, - ) - - result = execute_admin_command( - collection, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_", {"a10": 1}], + } + ], + }, + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a10": 1}, + "$db": ctx.database, + } + } + ], + msg="update setQuerySettings should succeed", + ), + SettingsTestCase( + "update_via_hash", + setup_commands=lambda ctx: [ { - "setQuerySettings": query, + "setQuerySettings": { + "find": ctx.collection, + "filter": {"a11": 1}, + "$db": ctx.database, + }, "settings": { "indexHints": [ { "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_", {"a10": 1}], + "allowedIndexes": ["_id_"], } ], }, + } + ], + command=lambda ctx: { + "setQuerySettings": ctx.setup_results[0]["queryShapeHash"], + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_", {"a11": 1}], + } + ], }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="update setQuerySettings should succeed") - finally: - cleanup_query_settings(collection, [query]) + }, + expected={"ok": 1.0}, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a11": 1}, + "$db": ctx.database, + } + } + ], + msg="update via hash should succeed", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_update_via_hash(collection): - """Test setQuerySettings can update settings using the query shape hash.""" +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_UPDATE_TESTS)) +def test_setQuerySettings_update(collection, test): + """Test setQuerySettings can update existing settings by query or hash.""" ctx = CommandContext.from_collection(collection) - query = { - "find": ctx.collection, - "filter": {"a11": 1}, - "$db": ctx.database, - } try: - setup_result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) - - query_hash = setup_result.get("queryShapeHash") - result = execute_admin_command( - collection, - { - "setQuerySettings": query_hash, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_", {"a11": 1}], - } - ], - }, - }, - ) - assertSuccessPartial(result, {"ok": 1.0}, msg="update via hash should succeed") + 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: - cleanup_query_settings(collection, [query]) + 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/setQuerySettings/test_setQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py index 4dd1cbc6b..cead12f5f 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py @@ -9,7 +9,6 @@ from __future__ import annotations import pytest -from pymongo.collection import Collection from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, @@ -26,16 +25,13 @@ QUERYSETTINGS_INTERNAL_DB_ERROR, QUERYSETTINGS_NS_COLL_MISSING_ERROR, QUERYSETTINGS_NS_DB_MISSING_ERROR, - QUERYSETTINGS_QUERY_REJECTED_ERROR, QUERYSETTINGS_REJECT_ONLY_ERROR, QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, UNRECOGNIZED_COMMAND_FIELD_ERROR, ) -from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params -from .utils.setQuerySettings_common import cleanup_query_settings - # Property [Query Shape Validation]: rejects malformed or unknown query shape documents. # Property [Hash String Validation]: rejects invalid hash string formats. # Property [indexHints Structure Validation]: rejects indexHints missing required sub-fields. @@ -428,38 +424,3 @@ def test_setQuerySettings_validation_errors(collection, test): error_code=test.error_code, msg=test.msg, ) - - -# Property [Reject Blocks Query]: a rejected query returns an error when executed. -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_reject_true_blocks_query(collection: Collection): - """Test that reject: true causes the matching query to be rejected.""" - query = { - "find": collection.name, - "filter": {"b8": 1}, - "$db": collection.database.name, - } - try: - execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": {"reject": True}, - }, - ) - - result = execute_command( - collection, - { - "find": collection.name, - "filter": {"b8": 1}, - }, - ) - assertResult( - result, - error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, - msg="query matching reject: true setting should be rejected", - ) - finally: - cleanup_query_settings(collection, [query]) diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py index de82b5544..113da8840 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py @@ -22,7 +22,7 @@ from documentdb_tests.framework.executor import execute_admin_command from documentdb_tests.framework.parametrize import pytest_params -from .utils.setQuerySettings_common import cleanup_query_settings, get_query_settings +from .utils.setQuerySettings_common import get_query_settings # Property [Hash Format]: queryShapeHash is a 64-character hexadecimal string. # Property [Hash Consistency]: same query shape produces the same hash. @@ -325,6 +325,7 @@ def test_setQuerySettings_hash_same(collection, test): setup_hash = None for cmd in test.build_setup(ctx): r = execute_admin_command(collection, cmd) + ctx.setup_results.append(r) if "queryShapeHash" in r: setup_hash = r["queryShapeHash"] result = execute_admin_command(collection, test.build_command(ctx)) @@ -351,6 +352,7 @@ def test_setQuerySettings_hash_different(collection, test): setup_hash = None for cmd in test.build_setup(ctx): r = execute_admin_command(collection, cmd) + ctx.setup_results.append(r) if "queryShapeHash" in r: setup_hash = r["queryShapeHash"] result = execute_admin_command(collection, test.build_command(ctx)) @@ -369,44 +371,62 @@ def test_setQuerySettings_hash_different(collection, test): # --------------------------------------------------------------------------- -# Group 3: Hash format test (standalone — regex check on response) +# Group 3: Hash format test # --------------------------------------------------------------------------- +SET_QUERY_SETTINGS_HASH_FORMAT_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( + "hash_is_64_char_hex", + command=lambda ctx: { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"h1": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"h1": 1}, + "$db": ctx.database, + } + } + ], + msg="queryShapeHash should be 64-char hex", + ), +] + @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_hash_is_64_char_hex(collection): - """Test that queryShapeHash is a 64-character hexadecimal string.""" +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_HASH_FORMAT_TESTS)) +def test_setQuerySettings_hash_format(collection, test): + """Test that queryShapeHash matches expected format.""" ctx = CommandContext.from_collection(collection) - query = { - "find": ctx.collection, - "filter": {"h1": 1}, - "$db": ctx.database, - } try: - result = execute_admin_command( - collection, - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ) + result = execute_admin_command(collection, test.build_command(ctx)) h = result.get("queryShapeHash", "") is_valid = bool(re.fullmatch(r"[0-9A-Fa-f]{64}", h)) assertSuccessPartial( {"valid": is_valid}, {"valid": True}, - msg=f"queryShapeHash should be 64-char hex, got: {h!r}", + msg=f"{test.msg}, got: {h!r}", ) finally: - cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass # --------------------------------------------------------------------------- @@ -744,7 +764,8 @@ def test_setQuerySettings_field_verification(collection, test): ctx = CommandContext.from_collection(collection) try: for cmd in test.build_setup(ctx): - execute_admin_command(collection, cmd) + r = execute_admin_command(collection, cmd) + ctx.setup_results.append(r) r = execute_admin_command(collection, test.build_command(ctx)) settings = get_query_settings(collection) matching = [s for s in settings if s.get("queryShapeHash") == r["queryShapeHash"]] @@ -763,134 +784,187 @@ def test_setQuerySettings_field_verification(collection, test): # --------------------------------------------------------------------------- -# Group 7: No duplicate on update test (standalone) +# Group 7: Multi-setup settings management tests # --------------------------------------------------------------------------- -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_no_duplicate_on_update(collection): - """Test that updating same shape does not create duplicate entries.""" - ctx = CommandContext.from_collection(collection) - query = { - "find": ctx.collection, - "filter": {"dup1": 1}, - "$db": ctx.database, - } - hints = { - "indexHints": [ +SET_QUERY_SETTINGS_MULTI_SETUP_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( + "no_duplicate_on_update", + setup_commands=lambda ctx: [ { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], + "setQuerySettings": { + "find": ctx.collection, + "filter": {"dup1": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"dup1": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + "queryFramework": "classic", + }, + }, + ], + expected=lambda ctx: { + "ok": sum( + 1 + for h in ctx.setup_results[-1]["_live_hashes"] + if h + == [r["queryShapeHash"] for r in ctx.setup_results if "queryShapeHash" in r][-1] + ) + == 1 + }, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"dup1": 1}, + "$db": ctx.database, + } } ], - } - try: - execute_admin_command( - collection, - {"setQuerySettings": query, "settings": hints}, - ) - r = execute_admin_command( - collection, - {"setQuerySettings": query, "settings": {**hints, "queryFramework": "classic"}}, - ) - all_settings = get_query_settings(collection) - count = sum(1 for s in all_settings if s.get("queryShapeHash") == r["queryShapeHash"]) - assertSuccessPartial( - {"count": count}, - {"count": 1}, - msg="updating same shape should not create duplicate entries", - ) - finally: - cleanup_query_settings(collection, [query]) - - -# --------------------------------------------------------------------------- -# Group 8: Multiple settings management test (standalone) -# --------------------------------------------------------------------------- - - -@pytest.mark.admin -@pytest.mark.replica_set -def test_setQuerySettings_multiple_settings_all_visible(collection): - """Test that multiple query settings are independently visible.""" - ctx = CommandContext.from_collection(collection) - queries = [ - {"find": ctx.collection, "filter": {"multi1": 1}, "$db": ctx.database}, - {"find": ctx.collection, "filter": {"multi2": 1}, "$db": ctx.database}, - {"find": ctx.collection, "filter": {"multi3": 1}, "$db": ctx.database}, - ] - hints = { - "indexHints": [ + msg="updating same shape should not create duplicate entries", + ), + SettingsTestCase( + "multiple_settings_all_visible", + setup_commands=lambda ctx: [ { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], + "setQuerySettings": { + "find": ctx.collection, + "filter": {f"multi{i}": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, } + for i in range(1, 4) ], - } - try: - hashes = [] - for q in queries: - r = execute_admin_command( - collection, - {"setQuerySettings": q, "settings": hints}, + expected=lambda ctx: { + "ok": all( + h in ctx.setup_results[-1]["_live_hashes"] + for h in [r["queryShapeHash"] for r in ctx.setup_results if "queryShapeHash" in r] ) - hashes.append(r["queryShapeHash"]) - all_hashes = {s.get("queryShapeHash") for s in get_query_settings(collection)} - all_present = all(h in all_hashes for h in hashes) - assertSuccessPartial( - {"all_present": all_present}, - {"all_present": True}, - msg="all 3 query settings should be visible in $querySettings", - ) - finally: - cleanup_query_settings(collection, queries) - - -# --------------------------------------------------------------------------- -# Group 9: Remove one leaves others test (standalone) -# --------------------------------------------------------------------------- + }, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {f"multi{i}": 1}, + "$db": ctx.database, + } + } + for i in range(1, 4) + ], + msg="all 3 query settings should be visible in $querySettings", + ), + SettingsTestCase( + "remove_one_leaves_others", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"rem1": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"rem2": 1}, + "$db": ctx.database, + }, + "settings": { + "indexHints": [ + { + "ns": {"db": ctx.database, "coll": ctx.collection}, + "allowedIndexes": ["_id_"], + } + ], + }, + }, + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"rem1": 1}, + "$db": ctx.database, + } + }, + ], + expected=lambda ctx: { + "ok": [r["queryShapeHash"] for r in ctx.setup_results if "queryShapeHash" in r][0] + not in ctx.setup_results[-1]["_live_hashes"] + and [r["queryShapeHash"] for r in ctx.setup_results if "queryShapeHash" in r][1] + in ctx.setup_results[-1]["_live_hashes"] + }, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {f"rem{i}": 1}, + "$db": ctx.database, + } + } + for i in range(1, 3) + ], + msg="q1 removed, q2 should remain in $querySettings", + ), +] @pytest.mark.admin @pytest.mark.replica_set -def test_setQuerySettings_remove_one_leaves_others(collection): - """Test that removing one setting leaves the others intact.""" +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_MULTI_SETUP_TESTS)) +def test_setQuerySettings_multi_setup(collection, test): + """Test multi-setup settings management via $querySettings inspection.""" ctx = CommandContext.from_collection(collection) - q1 = { - "find": ctx.collection, - "filter": {"rem1": 1}, - "$db": ctx.database, - } - q2 = { - "find": ctx.collection, - "filter": {"rem2": 1}, - "$db": ctx.database, - } - hints = { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - } try: - r1 = execute_admin_command( - collection, - {"setQuerySettings": q1, "settings": hints}, - ) - r2 = execute_admin_command( - collection, - {"setQuerySettings": q2, "settings": hints}, - ) - execute_admin_command(collection, {"removeQuerySettings": q1}) - remaining = {s.get("queryShapeHash") for s in get_query_settings(collection)} - correct = r1["queryShapeHash"] not in remaining and r2["queryShapeHash"] in remaining + for cmd in test.build_setup(ctx): + r = execute_admin_command(collection, cmd) + ctx.setup_results.append(r) + all_hashes = {s.get("queryShapeHash") for s in get_query_settings(collection)} + # Stash live hashes so expected-lambdas can reference them. + ctx.setup_results.append({"_live_hashes": all_hashes}) assertSuccessPartial( - {"correct": correct}, - {"correct": True}, - msg="q1 removed, q2 should remain in $querySettings", + test.build_expected(ctx), + {"ok": True}, + msg=test.msg, ) finally: - cleanup_query_settings(collection, [q1, q2]) + 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/utils/settings_test_case.py b/documentdb_tests/compatibility/tests/core/query_planning/utils/settings_test_case.py index 64eef24a5..9e8d565bc 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/utils/settings_test_case.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/utils/settings_test_case.py @@ -13,7 +13,7 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( @@ -29,20 +29,16 @@ class SettingsTestCase(CommandTestCase): Attributes: setup_commands: Optional callable ``(CommandContext) -> list[dict]`` returning commands to execute **before** the main command. - Each command's result is appended to ``setup_results`` - by the runner. + Each command's result is appended to + ``CommandContext.setup_results`` by the runner. cleanup: Optional callable ``(CommandContext) -> list[dict]`` returning commands to run after the test. Each dict is passed to the executor inside a try/except so cleanup failures are silently ignored. - setup_results: Results from setup commands, populated by the - runner. Mutable even in a frozen dataclass so runners can - append after construction. """ setup_commands: Callable[[CommandContext], list[dict[str, Any]]] | None = None cleanup: Callable[[CommandContext], list[dict[str, Any]]] | None = None - setup_results: list[dict[str, Any]] = field(default_factory=list) def build_setup(self, ctx: CommandContext) -> list[dict[str, Any]]: """Resolve setup commands from the callable, or return empty list.""" diff --git a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py index 121027368..27a686422 100644 --- a/documentdb_tests/compatibility/tests/core/utils/command_test_case.py +++ b/documentdb_tests/compatibility/tests/core/utils/command_test_case.py @@ -26,12 +26,16 @@ class CommandContext: database: The resolved database name. namespace: The full namespace string (``database.collection``). uuids: Mapping of collection names to their server-assigned UUIDs. + setup_results: Results from setup commands, populated by the runner. + Mutable even in a frozen dataclass so runners can append after + construction. """ collection: str database: str namespace: str uuids: dict[str, Any] = field(default_factory=dict) + setup_results: list[dict[str, Any]] = field(default_factory=list) @classmethod def from_collection(cls, collection: Collection) -> CommandContext: From 7f51ca94414d804f4338350b1907eb4e96b6b8e9 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 16 Jun 2026 11:44:09 -0700 Subject: [PATCH 18/27] (from setQuerySettings) split reject error cases Signed-off-by: Alina (Xi) Li --- .../test_setQuerySettings_reject.py | 123 +--------------- .../test_setQuerySettings_reject_errors.py | 134 ++++++++++++++++++ 2 files changed, 137 insertions(+), 120 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject_errors.py diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py index a47c0f318..5b4d2d16e 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py @@ -1,7 +1,6 @@ -"""Tests for setQuerySettings reject field behavior. +"""Tests for setQuerySettings reject field success behavior. -Validates that reject: true blocks matching queries for find, distinct, and -aggregate commands, that rejection does not affect unrelated query shapes, +Validates that rejection does not affect unrelated query shapes, and that reject can be reversed via update or removal. """ @@ -15,110 +14,14 @@ from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( CommandContext, ) -from documentdb_tests.framework.assertions import assertResult, assertSuccessPartial -from documentdb_tests.framework.error_codes import QUERYSETTINGS_QUERY_REJECTED_ERROR +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 -# Property [Reject Blocks Distinct]: reject: true blocks matching distinct queries. -# Property [Reject Blocks Aggregate]: reject: true blocks matching aggregate queries. # Property [Reject Scope]: reject: true does not affect unrelated query shapes. # Property [Reject Reversal via Update]: updating reject to false re-enables the query. # Property [Reject Reversal via Remove]: removing the query setting re-enables the query. # Property [Reject False Succeeds]: reject: false with indexHints allows the query. - -SET_QUERY_SETTINGS_REJECT_ERROR_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "reject_blocks_find", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"b8": 1}, - "$db": ctx.database, - }, - "settings": {"reject": True}, - } - ], - command=lambda ctx: { - "find": ctx.collection, - "filter": {"b8": 1}, - }, - error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"b8": 1}, - "$db": ctx.database, - } - } - ], - msg="query matching reject: true setting should be rejected", - ), - SettingsTestCase( - "reject_blocks_distinct", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "distinct": ctx.collection, - "key": "x", - "query": {"rej_d1": 1}, - "$db": ctx.database, - }, - "settings": {"reject": True}, - } - ], - command=lambda ctx: { - "distinct": ctx.collection, - "key": "x", - "query": {"rej_d1": 1}, - }, - error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "distinct": ctx.collection, - "key": "x", - "query": {"rej_d1": 1}, - "$db": ctx.database, - } - } - ], - msg="distinct query matching reject: true should be rejected", - ), - SettingsTestCase( - "reject_blocks_aggregate", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"rej_a1": 1}}], - "$db": ctx.database, - }, - "settings": {"reject": True}, - } - ], - command=lambda ctx: { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"rej_a1": 1}}], - "cursor": {}, - }, - error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"rej_a1": 1}}], - "$db": ctx.database, - } - } - ], - msg="aggregate query matching reject: true should be rejected", - ), -] - - SET_QUERY_SETTINGS_REJECT_SUCCESS_TESTS: list[SettingsTestCase] = [ SettingsTestCase( "reject_does_not_affect_different_shape", @@ -266,26 +169,6 @@ ] -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_REJECT_ERROR_TESTS)) -def test_setQuerySettings_reject_errors(collection, test): - """Test that reject: true blocks matching queries.""" - 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_command(collection, test.build_command(ctx)) - assertResult(result, error_code=test.error_code, msg=test.msg) - finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass - - @pytest.mark.admin @pytest.mark.replica_set @pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_REJECT_SUCCESS_TESTS)) diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject_errors.py new file mode 100644 index 000000000..ff0605fb4 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject_errors.py @@ -0,0 +1,134 @@ +"""Tests for setQuerySettings reject field error behavior. + +Validates that reject: true blocks matching queries for find, distinct, and +aggregate commands at execution time. +""" + +from __future__ import annotations + +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 assertResult +from documentdb_tests.framework.error_codes import QUERYSETTINGS_QUERY_REJECTED_ERROR +from documentdb_tests.framework.executor import execute_admin_command, execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Reject Blocks Find]: reject: true blocks matching find queries. +# Property [Reject Blocks Distinct]: reject: true blocks matching distinct queries. +# Property [Reject Blocks Aggregate]: reject: true blocks matching aggregate queries. +SET_QUERY_SETTINGS_REJECT_ERROR_TESTS: list[SettingsTestCase] = [ + SettingsTestCase( + "reject_blocks_find", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "find": ctx.collection, + "filter": {"b8": 1}, + "$db": ctx.database, + }, + "settings": {"reject": True}, + } + ], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"b8": 1}, + }, + error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b8": 1}, + "$db": ctx.database, + } + } + ], + msg="query matching reject: true setting should be rejected", + ), + SettingsTestCase( + "reject_blocks_distinct", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "query": {"rej_d1": 1}, + "$db": ctx.database, + }, + "settings": {"reject": True}, + } + ], + command=lambda ctx: { + "distinct": ctx.collection, + "key": "x", + "query": {"rej_d1": 1}, + }, + error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "query": {"rej_d1": 1}, + "$db": ctx.database, + } + } + ], + msg="distinct query matching reject: true should be rejected", + ), + SettingsTestCase( + "reject_blocks_aggregate", + setup_commands=lambda ctx: [ + { + "setQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"rej_a1": 1}}], + "$db": ctx.database, + }, + "settings": {"reject": True}, + } + ], + command=lambda ctx: { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"rej_a1": 1}}], + "cursor": {}, + }, + error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, + cleanup=lambda ctx: [ + { + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"rej_a1": 1}}], + "$db": ctx.database, + } + } + ], + msg="aggregate query matching reject: true should be rejected", + ), +] + + +@pytest.mark.admin +@pytest.mark.replica_set +@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_REJECT_ERROR_TESTS)) +def test_setQuerySettings_reject_errors(collection, test): + """Test that reject: true blocks matching queries.""" + 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_command(collection, test.build_command(ctx)) + assertResult(result, error_code=test.error_code, msg=test.msg) + finally: + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass From 44768f3351d08dc870538d94ed7e2f7b4ddac9c8 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 11:41:45 -0700 Subject: [PATCH 19/27] initially generated tests Signed-off-by: Alina (Xi) Li --- .../test_removeQuerySettings_behavior.py | 197 +++++++++ .../test_removeQuerySettings_core.py | 384 ++++++++++++++++++ .../test_removeQuerySettings_type_errors.py | 72 ++++ ...t_removeQuerySettings_validation_errors.py | 139 +++++++ 4 files changed, 792 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_behavior.py create mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_core.py create mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_type_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_validation_errors.py 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..4cb01443c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_behavior.py @@ -0,0 +1,197 @@ +"""Tests for removeQuerySettings command behavioral verification. + +Validates that removeQuerySettings actually removes query settings from the +cluster. Verifies removal by query shape and by hash, confirms idempotent +behavior, and checks that removed settings are no longer visible via the +$querySettings aggregation stage. +""" + +from __future__ import annotations + +from typing import Any + +import pytest +from pymongo.collection import Collection + +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_admin_command + +pytestmark = [pytest.mark.no_parallel] + + +def _get_query_settings(collection: Collection) -> list[dict[str, Any]]: + """Retrieve all current query settings via $querySettings stage.""" + admin = collection.database.client.admin + result = admin.command({"aggregate": 1, "pipeline": [{"$querySettings": {}}], "cursor": {}}) + batch: list[dict[str, Any]] = result.get("cursor", {}).get("firstBatch", []) + return batch + + +def _cleanup_query_settings(collection: Collection, queries: list[dict[str, Any]]) -> None: + """Remove all query settings created during a test.""" + admin = collection.database.client.admin + for q in queries: + try: + admin.command({"removeQuerySettings": q}) + except Exception: + pass + + +def _create_setting(collection: Collection, query: dict[str, Any]) -> str: + """Create a query setting and return the hash. Raises on failure.""" + admin = collection.database.client.admin + result = admin.command( + { + "setQuerySettings": query, + "settings": { + "indexHints": [ + { + "ns": { + "db": collection.database.name, + "coll": collection.name, + }, + "allowedIndexes": ["_id_"], + } + ] + }, + } + ) + query_hash: str = result.get("queryShapeHash", "") + if not query_hash: + raise RuntimeError("Precondition failed: setQuerySettings did not return hash") + return query_hash + + +# Property [Remove By Query Shape]: settings created with setQuerySettings +# can be removed by providing the same query shape to removeQuerySettings, +# and the setting is no longer visible via $querySettings. +@pytest.mark.replica_set +def test_removeQuerySettings_removes_by_query_shape(collection: Collection): + """Test removeQuerySettings removes settings by query shape.""" + query = { + "find": collection.name, + "filter": {"r1": 1}, + "$db": collection.database.name, + } + try: + expected_hash = _create_setting(collection, query) + + execute_admin_command(collection, {"removeQuerySettings": query}) + + # Verify the setting was actually removed. + after = _get_query_settings(collection) + matching = [s for s in after if s.get("queryShapeHash") == expected_hash] + assertSuccessPartial( + {"count": len(matching)}, + {"count": 0}, + msg="setting should be absent after removal by query shape", + ) + finally: + _cleanup_query_settings(collection, [query]) + + +# Property [Remove By Hash]: settings can be removed by providing the query +# shape hash string returned by setQuerySettings, and the setting is no longer +# visible via $querySettings. +@pytest.mark.replica_set +def test_removeQuerySettings_removes_by_hash(collection: Collection): + """Test removeQuerySettings removes settings by query shape hash.""" + query = { + "find": collection.name, + "filter": {"r2": 1}, + "$db": collection.database.name, + } + try: + query_hash = _create_setting(collection, query) + + execute_admin_command(collection, {"removeQuerySettings": query_hash}) + + # Verify the setting was actually removed. + after = _get_query_settings(collection) + matching = [s for s in after if s.get("queryShapeHash") == query_hash] + assertSuccessPartial( + {"count": len(matching)}, + {"count": 0}, + msg="setting should be absent after removal by hash", + ) + finally: + _cleanup_query_settings(collection, [query]) + + +# Property [Idempotent Removal]: calling removeQuerySettings a second time +# for the same query shape succeeds silently without error. +@pytest.mark.replica_set +def test_removeQuerySettings_idempotent(collection: Collection): + """Test removeQuerySettings is idempotent (succeeds on second call).""" + query = { + "find": collection.name, + "filter": {"r3": 1}, + "$db": collection.database.name, + } + try: + _create_setting(collection, query) + execute_admin_command(collection, {"removeQuerySettings": query}) + + # Second removal should still succeed. + result = execute_admin_command(collection, {"removeQuerySettings": query}) + assertSuccessPartial( + result, + {"ok": 1.0}, + msg="second removeQuerySettings should succeed silently", + ) + finally: + _cleanup_query_settings(collection, [query]) + + +# Property [Remove Distinct Shape]: removeQuerySettings can remove settings +# for distinct query shapes, verified by absence in $querySettings. +@pytest.mark.replica_set +def test_removeQuerySettings_removes_distinct_shape(collection: Collection): + """Test removeQuerySettings removes settings for a distinct query shape.""" + query = { + "distinct": collection.name, + "key": "x", + "$db": collection.database.name, + } + try: + expected_hash = _create_setting(collection, query) + + execute_admin_command(collection, {"removeQuerySettings": query}) + + # Verify the setting was actually removed. + after = _get_query_settings(collection) + matching = [s for s in after if s.get("queryShapeHash") == expected_hash] + assertSuccessPartial( + {"count": len(matching)}, + {"count": 0}, + msg="distinct setting should be absent after removal", + ) + finally: + _cleanup_query_settings(collection, [query]) + + +# Property [Remove Aggregate Shape]: removeQuerySettings can remove settings +# for aggregate query shapes, verified by absence in $querySettings. +@pytest.mark.replica_set +def test_removeQuerySettings_removes_aggregate_shape(collection: Collection): + """Test removeQuerySettings removes settings for an aggregate query shape.""" + query = { + "aggregate": collection.name, + "pipeline": [{"$match": {"x": 1}}], + "$db": collection.database.name, + } + try: + expected_hash = _create_setting(collection, query) + + execute_admin_command(collection, {"removeQuerySettings": query}) + + # Verify the setting was actually removed. + after = _get_query_settings(collection) + matching = [s for s in after if s.get("queryShapeHash") == expected_hash] + assertSuccessPartial( + {"count": len(matching)}, + {"count": 0}, + msg="aggregate setting should be absent after removal", + ) + finally: + _cleanup_query_settings(collection, [query]) 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..d61ad9b48 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_core.py @@ -0,0 +1,384 @@ +"""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, and hash-based removal. +""" + +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.no_parallel] + + +# Property [Command Shape Acceptance]: removeQuerySettings accepts find, +# distinct, and aggregate query shapes and succeeds even when no matching +# settings exist (idempotent). +REMOVE_QUERY_SETTINGS_COMMAND_SHAPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "find_shape", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"x": 1}, + "sort": {"x": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should accept valid find shape", + ), + CommandTestCase( + "distinct_shape", + command=lambda ctx: { + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "query": {"x": {"$gt": 0}}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should accept valid distinct shape", + ), + CommandTestCase( + "aggregate_shape", + command=lambda ctx: { + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"x": 1}}], + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should accept valid aggregate shape", + ), +] + +# Property [Find Shape Variations]: removeQuerySettings accepts find shapes +# with various field combinations. +REMOVE_QUERY_SETTINGS_FIND_VARIATION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "find_filter_only", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"a": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should accept find with filter only", + ), + CommandTestCase( + "find_filter_sort", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"b": 1}, + "sort": {"b": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should accept find with filter and sort", + ), + CommandTestCase( + "find_filter_projection", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"c": 1}, + "projection": {"c": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should accept find with filter and projection", + ), + CommandTestCase( + "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="should accept find with all shape fields", + ), + CommandTestCase( + "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="should accept find with collation", + ), + CommandTestCase( + "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="should accept find with let", + ), + CommandTestCase( + "find_with_limit", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"g": 1}, + "limit": 10, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should accept find with limit", + ), +] + +# Property [Distinct Shape Variations]: removeQuerySettings accepts distinct +# shapes with various field combinations. +REMOVE_QUERY_SETTINGS_DISTINCT_VARIATION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "distinct_key_only", + command=lambda ctx: { + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "j", + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should accept distinct with key only", + ), + CommandTestCase( + "distinct_complex_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="should accept distinct with complex query", + ), + CommandTestCase( + "distinct_with_collation", + command=lambda ctx: { + "removeQuerySettings": { + "distinct": ctx.collection, + "key": "x", + "query": {"x": 1}, + "collation": {"locale": "en"}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should accept distinct with collation", + ), +] + +# Property [Aggregate Shape Variations]: removeQuerySettings accepts aggregate +# pipeline shapes with various stage combinations. +REMOVE_QUERY_SETTINGS_AGGREGATE_VARIATION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "aggregate_match_only", + command=lambda ctx: { + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [{"$match": {"l": 1}}], + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should accept aggregate with $match only", + ), + CommandTestCase( + "aggregate_match_group", + 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="should accept aggregate with $match and $group", + ), + CommandTestCase( + "aggregate_match_sort_limit", + command=lambda ctx: { + "removeQuerySettings": { + "aggregate": ctx.collection, + "pipeline": [ + {"$match": {"n": 1}}, + {"$sort": {"n": 1}}, + {"$limit": 5}, + ], + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should accept aggregate with $match, $sort, and $limit", + ), +] + +# Property [$db Field Variations]: removeQuerySettings accepts non-existent +# and special-character database names in $db. +REMOVE_QUERY_SETTINGS_DB_VARIATION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "db_nonexistent", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"o": 1}, + "$db": "nonexistent_db_for_remove_query_settings_test", + } + }, + expected={"ok": 1.0}, + msg="should accept non-existent $db", + ), + CommandTestCase( + "db_special_characters", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"p": 1}, + "$db": "test-special-db", + } + }, + expected={"ok": 1.0}, + msg="should accept $db with special characters", + ), +] + +# Property [Idempotent Removal]: removeQuerySettings succeeds silently when +# no matching settings exist. +REMOVE_QUERY_SETTINGS_IDEMPOTENT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "nonexistent_query_shape", + command=lambda ctx: { + "removeQuerySettings": { + "find": ctx.collection, + "filter": {"nonexistent_field": 1}, + "$db": ctx.database, + } + }, + expected={"ok": 1.0}, + msg="should succeed silently when no matching settings exist", + ), + CommandTestCase( + "nonexistent_hash", + command=lambda ctx: { + "removeQuerySettings": "00000000000000000000000000000000" + "00000000000000000000000000000000" + }, + expected={"ok": 1.0}, + msg="should succeed silently with a non-existent hash", + ), + CommandTestCase( + "lowercase_hash", + command=lambda ctx: { + "removeQuerySettings": "abcdef0123456789abcdef0123456789" + "abcdef0123456789abcdef0123456789" + }, + expected={"ok": 1.0}, + msg="should accept lowercase hex hash string", + ), +] + +# Property [IDHACK Query Acceptance]: unlike setQuerySettings, +# removeQuerySettings accepts IDHACK-eligible queries without error. +REMOVE_QUERY_SETTINGS_IDHACK_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "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 [System Collection Acceptance]: unlike setQuerySettings, +# removeQuerySettings accepts query shapes targeting system collections and +# internal databases without error. +REMOVE_QUERY_SETTINGS_SYSTEM_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "system_collection", + command=lambda ctx: { + "removeQuerySettings": { + "find": "system.users", + "filter": {}, + "$db": "admin", + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept system collection query shapes", + ), + CommandTestCase( + "local_database", + command=lambda ctx: { + "removeQuerySettings": { + "find": "oplog.rs", + "filter": {}, + "$db": "local", + } + }, + expected={"ok": 1.0}, + msg="removeQuerySettings should accept local database query shapes", + ), +] + +REMOVE_QUERY_SETTINGS_CORE_TESTS: list[CommandTestCase] = ( + REMOVE_QUERY_SETTINGS_COMMAND_SHAPE_TESTS + + REMOVE_QUERY_SETTINGS_FIND_VARIATION_TESTS + + REMOVE_QUERY_SETTINGS_DISTINCT_VARIATION_TESTS + + REMOVE_QUERY_SETTINGS_AGGREGATE_VARIATION_TESTS + + REMOVE_QUERY_SETTINGS_DB_VARIATION_TESTS + + REMOVE_QUERY_SETTINGS_IDEMPOTENT_TESTS + + REMOVE_QUERY_SETTINGS_IDHACK_TESTS + + REMOVE_QUERY_SETTINGS_SYSTEM_TESTS +) + + +@pytest.mark.replica_set +@pytest.mark.parametrize("test", pytest_params(REMOVE_QUERY_SETTINGS_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_type_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_type_errors.py new file mode 100644 index 000000000..01af4c5f0 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_type_errors.py @@ -0,0 +1,72 @@ +"""Tests for removeQuerySettings command BSON type rejection. + +Validates that the removeQuerySettings command rejects invalid BSON types +for the primary argument field. The command accepts only string (hash) or +document (query shape); all other BSON types are rejected with +TYPE_MISMATCH_ERROR. +""" + +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 TYPE_MISMATCH_ERROR +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.no_parallel] + +# Property [Primary Argument Type Rejection]: the removeQuerySettings field +# must be a document (query shape) or string (hash). All other BSON types +# are rejected with TYPE_MISMATCH_ERROR. +REMOVE_QUERY_SETTINGS_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()), + ] +] + +REMOVE_QUERY_SETTINGS_TYPE_ERROR_TESTS: list[CommandTestCase] = ( + REMOVE_QUERY_SETTINGS_PRIMARY_ARG_TYPE_TESTS +) + + +@pytest.mark.replica_set +@pytest.mark.parametrize("test", pytest_params(REMOVE_QUERY_SETTINGS_TYPE_ERROR_TESTS)) +def test_removeQuerySettings_type_errors(collection, test): + """Test removeQuerySettings BSON type rejection.""" + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + error_code=test.error_code, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_validation_errors.py new file mode 100644 index 000000000..392835390 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_validation_errors.py @@ -0,0 +1,139 @@ +"""Tests for removeQuerySettings command structural and validation errors. + +Validates that the removeQuerySettings command rejects malformed query shapes, +invalid hash strings, missing or empty $db, unknown command shapes, and +unrecognized top-level fields. +""" + +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 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, + UNRECOGNIZED_COMMAND_FIELD_ERROR, +) +from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = [pytest.mark.no_parallel] + +# Property [Query Shape Validation]: rejects malformed query shape documents. +# Property [Hash String Validation]: rejects invalid hash string formats. +# Property [Unrecognized Fields]: rejects unknown top-level command fields. +REMOVE_QUERY_SETTINGS_VALIDATION_ERROR_TESTS: list[CommandTestCase] = [ + 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", + ), + 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 hash string shorter than 64 characters", + ), + CommandTestCase( + "non_hex_hash_string", + command=lambda ctx: { + "removeQuerySettings": "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG" + "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG" + }, + error_code=BAD_VALUE_ERROR, + msg="removeQuerySettings should reject non-hex hash string", + ), + 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", + ), +] + + +@pytest.mark.replica_set +@pytest.mark.parametrize("test", pytest_params(REMOVE_QUERY_SETTINGS_VALIDATION_ERROR_TESTS)) +def test_removeQuerySettings_validation_errors(collection, test): + """Test removeQuerySettings structural and validation error rejection.""" + ctx = CommandContext.from_collection(collection) + result = execute_admin_command(collection, test.build_command(ctx)) + assertResult( + result, + error_code=test.error_code, + msg=test.msg, + ) From f872cd96ff6de2f794eed39bb3c84e4f8e3fbdb3 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Mon, 15 Jun 2026 12:15:52 -0700 Subject: [PATCH 20/27] apply style guide Signed-off-by: Alina (Xi) Li --- .../commands/removeQuerySettings/__init__.py | 0 .../test_removeQuerySettings_behavior.py | 77 +++++++-------- .../test_removeQuerySettings_core.py | 97 +++++++++---------- ...s.py => test_removeQuerySettings_error.py} | 70 ++++++++++--- .../test_removeQuerySettings_type_errors.py | 72 -------------- 5 files changed, 143 insertions(+), 173 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/__init__.py rename documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/{test_removeQuerySettings_validation_errors.py => test_removeQuerySettings_error.py} (64%) delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_type_errors.py 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 index 4cb01443c..cbeb35b65 100644 --- 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 @@ -1,9 +1,8 @@ """Tests for removeQuerySettings command behavioral verification. -Validates that removeQuerySettings actually removes query settings from the -cluster. Verifies removal by query shape and by hash, confirms idempotent -behavior, and checks that removed settings are no longer visible via the -$querySettings aggregation stage. +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 @@ -38,7 +37,10 @@ def _cleanup_query_settings(collection: Collection, queries: list[dict[str, Any] def _create_setting(collection: Collection, query: dict[str, Any]) -> str: - """Create a query setting and return the hash. Raises on failure.""" + """Create a query setting and return the hash. + + Raises RuntimeError if the precondition cannot be established. + """ admin = collection.database.client.admin result = admin.command( { @@ -62,9 +64,14 @@ def _create_setting(collection: Collection, query: dict[str, Any]) -> str: return query_hash -# Property [Remove By Query Shape]: settings created with setQuerySettings -# can be removed by providing the same query shape to removeQuerySettings, -# and the setting is no longer visible via $querySettings. +def _count_matching_settings(collection: Collection, query_hash: str) -> int: + """Count query settings matching the given hash.""" + settings = _get_query_settings(collection) + return sum(1 for s in settings if s.get("queryShapeHash") == query_hash) + + +# Property [Remove By Query Shape]: removeQuerySettings removes settings +# when given the original query shape, verified via $querySettings. @pytest.mark.replica_set def test_removeQuerySettings_removes_by_query_shape(collection: Collection): """Test removeQuerySettings removes settings by query shape.""" @@ -78,21 +85,17 @@ def test_removeQuerySettings_removes_by_query_shape(collection: Collection): execute_admin_command(collection, {"removeQuerySettings": query}) - # Verify the setting was actually removed. - after = _get_query_settings(collection) - matching = [s for s in after if s.get("queryShapeHash") == expected_hash] assertSuccessPartial( - {"count": len(matching)}, + {"count": _count_matching_settings(collection, expected_hash)}, {"count": 0}, - msg="setting should be absent after removal by query shape", + msg="removeQuerySettings should remove the setting by query shape", ) finally: _cleanup_query_settings(collection, [query]) -# Property [Remove By Hash]: settings can be removed by providing the query -# shape hash string returned by setQuerySettings, and the setting is no longer -# visible via $querySettings. +# Property [Remove By Hash]: removeQuerySettings removes settings when given +# the query shape hash string, verified via $querySettings. @pytest.mark.replica_set def test_removeQuerySettings_removes_by_hash(collection: Collection): """Test removeQuerySettings removes settings by query shape hash.""" @@ -106,13 +109,10 @@ def test_removeQuerySettings_removes_by_hash(collection: Collection): execute_admin_command(collection, {"removeQuerySettings": query_hash}) - # Verify the setting was actually removed. - after = _get_query_settings(collection) - matching = [s for s in after if s.get("queryShapeHash") == query_hash] assertSuccessPartial( - {"count": len(matching)}, + {"count": _count_matching_settings(collection, query_hash)}, {"count": 0}, - msg="setting should be absent after removal by hash", + msg="removeQuerySettings should remove the setting by hash", ) finally: _cleanup_query_settings(collection, [query]) @@ -122,7 +122,7 @@ def test_removeQuerySettings_removes_by_hash(collection: Collection): # for the same query shape succeeds silently without error. @pytest.mark.replica_set def test_removeQuerySettings_idempotent(collection: Collection): - """Test removeQuerySettings is idempotent (succeeds on second call).""" + """Test removeQuerySettings is idempotent on second call.""" query = { "find": collection.name, "filter": {"r3": 1}, @@ -132,21 +132,22 @@ def test_removeQuerySettings_idempotent(collection: Collection): _create_setting(collection, query) execute_admin_command(collection, {"removeQuerySettings": query}) - # Second removal should still succeed. result = execute_admin_command(collection, {"removeQuerySettings": query}) assertSuccessPartial( result, {"ok": 1.0}, - msg="second removeQuerySettings should succeed silently", + msg="removeQuerySettings should succeed silently on second removal", ) finally: _cleanup_query_settings(collection, [query]) -# Property [Remove Distinct Shape]: removeQuerySettings can remove settings -# for distinct query shapes, verified by absence in $querySettings. +# Property [Remove Distinct Shape]: removeQuerySettings removes settings for +# distinct query shapes, verified via $querySettings. @pytest.mark.replica_set -def test_removeQuerySettings_removes_distinct_shape(collection: Collection): +def test_removeQuerySettings_removes_distinct_shape( + collection: Collection, +): """Test removeQuerySettings removes settings for a distinct query shape.""" query = { "distinct": collection.name, @@ -158,22 +159,21 @@ def test_removeQuerySettings_removes_distinct_shape(collection: Collection): execute_admin_command(collection, {"removeQuerySettings": query}) - # Verify the setting was actually removed. - after = _get_query_settings(collection) - matching = [s for s in after if s.get("queryShapeHash") == expected_hash] assertSuccessPartial( - {"count": len(matching)}, + {"count": _count_matching_settings(collection, expected_hash)}, {"count": 0}, - msg="distinct setting should be absent after removal", + msg="removeQuerySettings should remove the distinct setting", ) finally: _cleanup_query_settings(collection, [query]) -# Property [Remove Aggregate Shape]: removeQuerySettings can remove settings -# for aggregate query shapes, verified by absence in $querySettings. +# Property [Remove Aggregate Shape]: removeQuerySettings removes settings for +# aggregate query shapes, verified via $querySettings. @pytest.mark.replica_set -def test_removeQuerySettings_removes_aggregate_shape(collection: Collection): +def test_removeQuerySettings_removes_aggregate_shape( + collection: Collection, +): """Test removeQuerySettings removes settings for an aggregate query shape.""" query = { "aggregate": collection.name, @@ -185,13 +185,10 @@ def test_removeQuerySettings_removes_aggregate_shape(collection: Collection): execute_admin_command(collection, {"removeQuerySettings": query}) - # Verify the setting was actually removed. - after = _get_query_settings(collection) - matching = [s for s in after if s.get("queryShapeHash") == expected_hash] assertSuccessPartial( - {"count": len(matching)}, + {"count": _count_matching_settings(collection, expected_hash)}, {"count": 0}, - msg="aggregate setting should be absent after removal", + msg="removeQuerySettings should remove the aggregate setting", ) finally: _cleanup_query_settings(collection, [query]) 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 index d61ad9b48..04360eb8e 100644 --- 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 @@ -2,7 +2,7 @@ Validates that the removeQuerySettings command accepts valid query shapes for find, distinct, and aggregate commands, various shape variations, -$db field variations, and hash-based removal. +$db field variations, hash-based removal, and idempotent behavior. """ from __future__ import annotations @@ -19,11 +19,9 @@ pytestmark = [pytest.mark.no_parallel] - # Property [Command Shape Acceptance]: removeQuerySettings accepts find, -# distinct, and aggregate query shapes and succeeds even when no matching -# settings exist (idempotent). -REMOVE_QUERY_SETTINGS_COMMAND_SHAPE_TESTS: list[CommandTestCase] = [ +# distinct, and aggregate query shapes. +REMOVEQUERYSETTINGS_COMMAND_SHAPE_TESTS: list[CommandTestCase] = [ CommandTestCase( "find_shape", command=lambda ctx: { @@ -35,7 +33,7 @@ } }, expected={"ok": 1.0}, - msg="should accept valid find shape", + msg="removeQuerySettings should accept valid find shape", ), CommandTestCase( "distinct_shape", @@ -48,7 +46,7 @@ } }, expected={"ok": 1.0}, - msg="should accept valid distinct shape", + msg="removeQuerySettings should accept valid distinct shape", ), CommandTestCase( "aggregate_shape", @@ -60,13 +58,13 @@ } }, expected={"ok": 1.0}, - msg="should accept valid aggregate shape", + msg="removeQuerySettings should accept valid aggregate shape", ), ] # Property [Find Shape Variations]: removeQuerySettings accepts find shapes # with various field combinations. -REMOVE_QUERY_SETTINGS_FIND_VARIATION_TESTS: list[CommandTestCase] = [ +REMOVEQUERYSETTINGS_FIND_VARIATION_TESTS: list[CommandTestCase] = [ CommandTestCase( "find_filter_only", command=lambda ctx: { @@ -77,7 +75,7 @@ } }, expected={"ok": 1.0}, - msg="should accept find with filter only", + msg="removeQuerySettings should accept find with filter only", ), CommandTestCase( "find_filter_sort", @@ -90,7 +88,7 @@ } }, expected={"ok": 1.0}, - msg="should accept find with filter and sort", + msg="removeQuerySettings should accept find with filter and sort", ), CommandTestCase( "find_filter_projection", @@ -103,7 +101,7 @@ } }, expected={"ok": 1.0}, - msg="should accept find with filter and projection", + msg="removeQuerySettings should accept find with filter and projection", ), CommandTestCase( "find_filter_sort_projection", @@ -117,7 +115,7 @@ } }, expected={"ok": 1.0}, - msg="should accept find with all shape fields", + msg="removeQuerySettings should accept find with all shape fields", ), CommandTestCase( "find_with_collation", @@ -130,7 +128,7 @@ } }, expected={"ok": 1.0}, - msg="should accept find with collation", + msg="removeQuerySettings should accept find with collation", ), CommandTestCase( "find_with_let", @@ -143,7 +141,7 @@ } }, expected={"ok": 1.0}, - msg="should accept find with let", + msg="removeQuerySettings should accept find with let", ), CommandTestCase( "find_with_limit", @@ -156,13 +154,13 @@ } }, expected={"ok": 1.0}, - msg="should accept find with limit", + msg="removeQuerySettings should accept find with limit", ), ] # Property [Distinct Shape Variations]: removeQuerySettings accepts distinct # shapes with various field combinations. -REMOVE_QUERY_SETTINGS_DISTINCT_VARIATION_TESTS: list[CommandTestCase] = [ +REMOVEQUERYSETTINGS_DISTINCT_VARIATION_TESTS: list[CommandTestCase] = [ CommandTestCase( "distinct_key_only", command=lambda ctx: { @@ -173,7 +171,7 @@ } }, expected={"ok": 1.0}, - msg="should accept distinct with key only", + msg="removeQuerySettings should accept distinct with key only", ), CommandTestCase( "distinct_complex_query", @@ -186,7 +184,7 @@ } }, expected={"ok": 1.0}, - msg="should accept distinct with complex query", + msg="removeQuerySettings should accept distinct with complex query", ), CommandTestCase( "distinct_with_collation", @@ -200,13 +198,13 @@ } }, expected={"ok": 1.0}, - msg="should accept distinct with collation", + msg="removeQuerySettings should accept distinct with collation", ), ] # Property [Aggregate Shape Variations]: removeQuerySettings accepts aggregate # pipeline shapes with various stage combinations. -REMOVE_QUERY_SETTINGS_AGGREGATE_VARIATION_TESTS: list[CommandTestCase] = [ +REMOVEQUERYSETTINGS_AGGREGATE_VARIATION_TESTS: list[CommandTestCase] = [ CommandTestCase( "aggregate_match_only", command=lambda ctx: { @@ -217,7 +215,7 @@ } }, expected={"ok": 1.0}, - msg="should accept aggregate with $match only", + msg="removeQuerySettings should accept aggregate with $match only", ), CommandTestCase( "aggregate_match_group", @@ -232,7 +230,7 @@ } }, expected={"ok": 1.0}, - msg="should accept aggregate with $match and $group", + msg="removeQuerySettings should accept aggregate with $match and $group", ), CommandTestCase( "aggregate_match_sort_limit", @@ -248,24 +246,24 @@ } }, expected={"ok": 1.0}, - msg="should accept aggregate with $match, $sort, and $limit", + msg="removeQuerySettings should accept aggregate with $match, $sort, and $limit", ), ] # Property [$db Field Variations]: removeQuerySettings accepts non-existent -# and special-character database names in $db. -REMOVE_QUERY_SETTINGS_DB_VARIATION_TESTS: list[CommandTestCase] = [ +# and special-character database names in the $db field. +REMOVEQUERYSETTINGS_DB_VARIATION_TESTS: list[CommandTestCase] = [ CommandTestCase( "db_nonexistent", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, "filter": {"o": 1}, - "$db": "nonexistent_db_for_remove_query_settings_test", + "$db": f"{ctx.database}_nonexistent", } }, expected={"ok": 1.0}, - msg="should accept non-existent $db", + msg="removeQuerySettings should accept non-existent $db", ), CommandTestCase( "db_special_characters", @@ -277,13 +275,13 @@ } }, expected={"ok": 1.0}, - msg="should accept $db with special characters", + msg="removeQuerySettings should accept $db with special characters", ), ] # Property [Idempotent Removal]: removeQuerySettings succeeds silently when # no matching settings exist. -REMOVE_QUERY_SETTINGS_IDEMPOTENT_TESTS: list[CommandTestCase] = [ +REMOVEQUERYSETTINGS_IDEMPOTENT_TESTS: list[CommandTestCase] = [ CommandTestCase( "nonexistent_query_shape", command=lambda ctx: { @@ -294,7 +292,7 @@ } }, expected={"ok": 1.0}, - msg="should succeed silently when no matching settings exist", + msg="removeQuerySettings should succeed when no matching settings exist", ), CommandTestCase( "nonexistent_hash", @@ -303,7 +301,7 @@ "00000000000000000000000000000000" }, expected={"ok": 1.0}, - msg="should succeed silently with a non-existent hash", + msg="removeQuerySettings should succeed with a non-existent hash", ), CommandTestCase( "lowercase_hash", @@ -312,13 +310,13 @@ "abcdef0123456789abcdef0123456789" }, expected={"ok": 1.0}, - msg="should accept lowercase hex hash string", + msg="removeQuerySettings should accept lowercase hex hash string", ), ] -# Property [IDHACK Query Acceptance]: unlike setQuerySettings, -# removeQuerySettings accepts IDHACK-eligible queries without error. -REMOVE_QUERY_SETTINGS_IDHACK_TESTS: list[CommandTestCase] = [ +# Property [IDHACK Query Acceptance]: removeQuerySettings accepts +# IDHACK-eligible queries without error. +REMOVEQUERYSETTINGS_IDHACK_TESTS: list[CommandTestCase] = [ CommandTestCase( "idhack_query", command=lambda ctx: { @@ -333,10 +331,9 @@ ), ] -# Property [System Collection Acceptance]: unlike setQuerySettings, -# removeQuerySettings accepts query shapes targeting system collections and -# internal databases without error. -REMOVE_QUERY_SETTINGS_SYSTEM_TESTS: list[CommandTestCase] = [ +# Property [System Collection Acceptance]: removeQuerySettings accepts query +# shapes targeting system collections and internal databases. +REMOVEQUERYSETTINGS_SYSTEM_TESTS: list[CommandTestCase] = [ CommandTestCase( "system_collection", command=lambda ctx: { @@ -363,20 +360,20 @@ ), ] -REMOVE_QUERY_SETTINGS_CORE_TESTS: list[CommandTestCase] = ( - REMOVE_QUERY_SETTINGS_COMMAND_SHAPE_TESTS - + REMOVE_QUERY_SETTINGS_FIND_VARIATION_TESTS - + REMOVE_QUERY_SETTINGS_DISTINCT_VARIATION_TESTS - + REMOVE_QUERY_SETTINGS_AGGREGATE_VARIATION_TESTS - + REMOVE_QUERY_SETTINGS_DB_VARIATION_TESTS - + REMOVE_QUERY_SETTINGS_IDEMPOTENT_TESTS - + REMOVE_QUERY_SETTINGS_IDHACK_TESTS - + REMOVE_QUERY_SETTINGS_SYSTEM_TESTS +REMOVEQUERYSETTINGS_CORE_TESTS: list[CommandTestCase] = ( + REMOVEQUERYSETTINGS_COMMAND_SHAPE_TESTS + + REMOVEQUERYSETTINGS_FIND_VARIATION_TESTS + + REMOVEQUERYSETTINGS_DISTINCT_VARIATION_TESTS + + REMOVEQUERYSETTINGS_AGGREGATE_VARIATION_TESTS + + REMOVEQUERYSETTINGS_DB_VARIATION_TESTS + + REMOVEQUERYSETTINGS_IDEMPOTENT_TESTS + + REMOVEQUERYSETTINGS_IDHACK_TESTS + + REMOVEQUERYSETTINGS_SYSTEM_TESTS ) @pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(REMOVE_QUERY_SETTINGS_CORE_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) diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_error.py similarity index 64% rename from documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_validation_errors.py rename to documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_error.py index 392835390..d115bf08c 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_validation_errors.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_error.py @@ -1,13 +1,16 @@ -"""Tests for removeQuerySettings command structural and validation errors. +"""Tests for removeQuerySettings command error cases. -Validates that the removeQuerySettings command rejects malformed query shapes, -invalid hash strings, missing or empty $db, unknown command shapes, and +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, @@ -20,6 +23,7 @@ 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 @@ -27,10 +31,38 @@ pytestmark = [pytest.mark.no_parallel] -# Property [Query Shape Validation]: rejects malformed query shape documents. -# Property [Hash String Validation]: rejects invalid hash string formats. -# Property [Unrecognized Fields]: rejects unknown top-level command fields. -REMOVE_QUERY_SETTINGS_VALIDATION_ERROR_TESTS: list[CommandTestCase] = [ +# 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 +# with missing, empty, or null $db and unknown command types. +REMOVEQUERYSETTINGS_QUERY_SHAPE_VALIDATION_TESTS: list[CommandTestCase] = [ CommandTestCase( "query_shape_missing_db", command=lambda ctx: { @@ -89,6 +121,11 @@ 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, and non-hexadecimal strings. +REMOVEQUERYSETTINGS_HASH_VALIDATION_TESTS: list[CommandTestCase] = [ CommandTestCase( "empty_hash_string", command=lambda ctx: {"removeQuerySettings": ""}, @@ -99,7 +136,7 @@ "short_hash_string", command=lambda ctx: {"removeQuerySettings": "ABCD"}, error_code=INVALID_LENGTH_ERROR, - msg="removeQuerySettings should reject hash string shorter than 64 characters", + msg="removeQuerySettings should reject short hash string", ), CommandTestCase( "non_hex_hash_string", @@ -110,6 +147,10 @@ error_code=BAD_VALUE_ERROR, msg="removeQuerySettings should reject non-hex hash string", ), +] + +# Property [Unrecognized Fields]: rejects unknown top-level command fields. +REMOVEQUERYSETTINGS_UNRECOGNIZED_FIELD_TESTS: list[CommandTestCase] = [ CommandTestCase( "unrecognized_top_level_field", command=lambda ctx: { @@ -125,11 +166,18 @@ ), ] +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.replica_set -@pytest.mark.parametrize("test", pytest_params(REMOVE_QUERY_SETTINGS_VALIDATION_ERROR_TESTS)) -def test_removeQuerySettings_validation_errors(collection, test): - """Test removeQuerySettings structural and validation error rejection.""" +@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( diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_type_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_type_errors.py deleted file mode 100644 index 01af4c5f0..000000000 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/removeQuerySettings/test_removeQuerySettings_type_errors.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Tests for removeQuerySettings command BSON type rejection. - -Validates that the removeQuerySettings command rejects invalid BSON types -for the primary argument field. The command accepts only string (hash) or -document (query shape); all other BSON types are rejected with -TYPE_MISMATCH_ERROR. -""" - -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 TYPE_MISMATCH_ERROR -from documentdb_tests.framework.executor import execute_admin_command -from documentdb_tests.framework.parametrize import pytest_params - -pytestmark = [pytest.mark.no_parallel] - -# Property [Primary Argument Type Rejection]: the removeQuerySettings field -# must be a document (query shape) or string (hash). All other BSON types -# are rejected with TYPE_MISMATCH_ERROR. -REMOVE_QUERY_SETTINGS_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()), - ] -] - -REMOVE_QUERY_SETTINGS_TYPE_ERROR_TESTS: list[CommandTestCase] = ( - REMOVE_QUERY_SETTINGS_PRIMARY_ARG_TYPE_TESTS -) - - -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(REMOVE_QUERY_SETTINGS_TYPE_ERROR_TESTS)) -def test_removeQuerySettings_type_errors(collection, test): - """Test removeQuerySettings BSON type rejection.""" - ctx = CommandContext.from_collection(collection) - result = execute_admin_command(collection, test.build_command(ctx)) - assertResult( - result, - error_code=test.error_code, - msg=test.msg, - ) From 57c095c00302aaf81ad46553f88c1f333123c3ad Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 16 Jun 2026 12:02:15 -0700 Subject: [PATCH 21/27] convert to use SettingsTestCase Signed-off-by: Alina (Xi) Li --- .../test_removeQuerySettings_behavior.py | 403 +++++++++++------- 1 file changed, 247 insertions(+), 156 deletions(-) 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 index cbeb35b65..57c62f1cb 100644 --- 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 @@ -10,185 +10,276 @@ from typing import Any import pytest -from pymongo.collection import Collection +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 +from documentdb_tests.framework.parametrize import pytest_params pytestmark = [pytest.mark.no_parallel] - -def _get_query_settings(collection: Collection) -> list[dict[str, Any]]: - """Retrieve all current query settings via $querySettings stage.""" - admin = collection.database.client.admin - result = admin.command({"aggregate": 1, "pipeline": [{"$querySettings": {}}], "cursor": {}}) - batch: list[dict[str, Any]] = result.get("cursor", {}).get("firstBatch", []) - return batch - - -def _cleanup_query_settings(collection: Collection, queries: list[dict[str, Any]]) -> None: - """Remove all query settings created during a test.""" - admin = collection.database.client.admin - for q in queries: - try: - admin.command({"removeQuerySettings": q}) - except Exception: - pass - - -def _create_setting(collection: Collection, query: dict[str, Any]) -> str: - """Create a query setting and return the hash. - - Raises RuntimeError if the precondition cannot be established. - """ - admin = collection.database.client.admin - result = admin.command( - { - "setQuerySettings": query, - "settings": { - "indexHints": [ - { - "ns": { - "db": collection.database.name, - "coll": collection.name, - }, - "allowedIndexes": ["_id_"], - } - ] - }, - } - ) - query_hash: str = result.get("queryShapeHash", "") - if not query_hash: - raise RuntimeError("Precondition failed: setQuerySettings did not return hash") - return query_hash - - -def _count_matching_settings(collection: Collection, query_hash: str) -> int: - """Count query settings matching the given hash.""" - settings = _get_query_settings(collection) - return sum(1 for s in settings if s.get("queryShapeHash") == query_hash) - - # Property [Remove By Query Shape]: removeQuerySettings removes settings # when given the original query shape, verified via $querySettings. -@pytest.mark.replica_set -def test_removeQuerySettings_removes_by_query_shape(collection: Collection): - """Test removeQuerySettings removes settings by query shape.""" - query = { - "find": collection.name, - "filter": {"r1": 1}, - "$db": collection.database.name, - } - try: - expected_hash = _create_setting(collection, query) - - execute_admin_command(collection, {"removeQuerySettings": query}) - - assertSuccessPartial( - {"count": _count_matching_settings(collection, expected_hash)}, - {"count": 0}, - msg="removeQuerySettings should remove the setting by query shape", - ) - finally: - _cleanup_query_settings(collection, [query]) - - # 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. +REMOVEQUERYSETTINGS_VERIFIED_REMOVAL_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", + ), +] + + +@pytest.mark.admin @pytest.mark.replica_set -def test_removeQuerySettings_removes_by_hash(collection: Collection): - """Test removeQuerySettings removes settings by query shape hash.""" - query = { - "find": collection.name, - "filter": {"r2": 1}, - "$db": collection.database.name, - } +@pytest.mark.parametrize("test", pytest_params(REMOVEQUERYSETTINGS_VERIFIED_REMOVAL_TESTS)) +def test_removeQuerySettings_verified_removal(collection, test): + """Test that removeQuerySettings actually removes settings, verified via $querySettings.""" + ctx = CommandContext.from_collection(collection) try: - query_hash = _create_setting(collection, query) + 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, {"removeQuerySettings": query_hash}) + 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_matching_settings(collection, query_hash)}, + {"count": count}, {"count": 0}, - msg="removeQuerySettings should remove the setting by hash", + msg=test.msg, ) finally: - _cleanup_query_settings(collection, [query]) + 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.replica_set -def test_removeQuerySettings_idempotent(collection: Collection): +@pytest.mark.parametrize("test", pytest_params(REMOVEQUERYSETTINGS_IDEMPOTENT_TESTS)) +def test_removeQuerySettings_idempotent(collection, test): """Test removeQuerySettings is idempotent on second call.""" - query = { - "find": collection.name, - "filter": {"r3": 1}, - "$db": collection.database.name, - } + ctx = CommandContext.from_collection(collection) try: - _create_setting(collection, query) - execute_admin_command(collection, {"removeQuerySettings": query}) - - result = execute_admin_command(collection, {"removeQuerySettings": query}) - assertSuccessPartial( - result, - {"ok": 1.0}, - msg="removeQuerySettings should succeed silently on second removal", - ) - finally: - _cleanup_query_settings(collection, [query]) - - -# Property [Remove Distinct Shape]: removeQuerySettings removes settings for -# distinct query shapes, verified via $querySettings. -@pytest.mark.replica_set -def test_removeQuerySettings_removes_distinct_shape( - collection: Collection, -): - """Test removeQuerySettings removes settings for a distinct query shape.""" - query = { - "distinct": collection.name, - "key": "x", - "$db": collection.database.name, - } - try: - expected_hash = _create_setting(collection, query) - - execute_admin_command(collection, {"removeQuerySettings": query}) - - assertSuccessPartial( - {"count": _count_matching_settings(collection, expected_hash)}, - {"count": 0}, - msg="removeQuerySettings should remove the distinct setting", - ) - finally: - _cleanup_query_settings(collection, [query]) - - -# Property [Remove Aggregate Shape]: removeQuerySettings removes settings for -# aggregate query shapes, verified via $querySettings. -@pytest.mark.replica_set -def test_removeQuerySettings_removes_aggregate_shape( - collection: Collection, -): - """Test removeQuerySettings removes settings for an aggregate query shape.""" - query = { - "aggregate": collection.name, - "pipeline": [{"$match": {"x": 1}}], - "$db": collection.database.name, - } - try: - expected_hash = _create_setting(collection, query) - - execute_admin_command(collection, {"removeQuerySettings": query}) - - assertSuccessPartial( - {"count": _count_matching_settings(collection, expected_hash)}, - {"count": 0}, - msg="removeQuerySettings should remove the aggregate setting", - ) + 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: - _cleanup_query_settings(collection, [query]) + for cmd in test.build_cleanup(ctx): + try: + execute_admin_command(collection, cmd) + except Exception: + pass From b126b8812e74e93016a4597a9fcc4d1febbefb15 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 16 Jun 2026 12:07:31 -0700 Subject: [PATCH 22/27] remove cleanup_query_settings Signed-off-by: Alina (Xi) Li --- .../setQuerySettings/utils/setQuerySettings_common.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/setQuerySettings_common.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/setQuerySettings_common.py index 3ae8667c8..9d5da0037 100644 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/setQuerySettings_common.py +++ b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/setQuerySettings_common.py @@ -7,16 +7,6 @@ from pymongo.collection import Collection -def cleanup_query_settings(collection: Collection, queries: list[dict]) -> None: - """Remove all query settings created during a test.""" - admin = collection.database.client.admin - for q in queries: - try: - admin.command({"removeQuerySettings": q}) - except Exception: - pass - - def get_query_settings(collection: Collection) -> list[dict[str, Any]]: """Retrieve all current query settings via $querySettings stage.""" admin = collection.database.client.admin From df92f2179875be574bcd0bef9200f8e3ff5f4757 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 16 Jun 2026 12:29:18 -0700 Subject: [PATCH 23/27] add missing test case Signed-off-by: Alina (Xi) Li --- .../test_removeQuerySettings_behavior.py | 338 +++++++++++++++++- .../test_removeQuerySettings_core.py | 19 + .../test_removeQuerySettings_error.py | 32 +- 3 files changed, 385 insertions(+), 4 deletions(-) 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 index 57c62f1cb..fe88786b4 100644 --- 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 @@ -18,7 +18,7 @@ CommandContext, ) from documentdb_tests.framework.assertions import assertSuccessPartial -from documentdb_tests.framework.executor import execute_admin_command +from documentdb_tests.framework.executor import execute_admin_command, execute_command from documentdb_tests.framework.parametrize import pytest_params pytestmark = [pytest.mark.no_parallel] @@ -283,3 +283,339 @@ def test_removeQuerySettings_idempotent(collection, test): execute_admin_command(collection, cmd) except Exception: pass + + +# Property [Shape Matching Ignores Filter Values]: query shape matching uses +# field structure, not values. Removing with different filter values removes +# the original setting. +REMOVEQUERYSETTINGS_SHAPE_REMOVES_TESTS: list[SettingsTestCase] = [ + 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.replica_set +@pytest.mark.parametrize("test", pytest_params(REMOVEQUERYSETTINGS_SHAPE_REMOVES_TESTS)) +def test_removeQuerySettings_shape_removes(collection, test): + """Test that shape matching ignores filter values — same shape is removed.""" + 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 [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.replica_set +@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.replica_set +@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 index 04360eb8e..41db5bc65 100644 --- 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 @@ -360,6 +360,24 @@ ), ] +# Property [Comment Field Acceptance]: removeQuerySettings accepts the +# comment field without error. +REMOVEQUERYSETTINGS_COMMENT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "comment_field_accepted", + 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_COMMAND_SHAPE_TESTS + REMOVEQUERYSETTINGS_FIND_VARIATION_TESTS @@ -369,6 +387,7 @@ + REMOVEQUERYSETTINGS_IDEMPOTENT_TESTS + REMOVEQUERYSETTINGS_IDHACK_TESTS + REMOVEQUERYSETTINGS_SYSTEM_TESTS + + REMOVEQUERYSETTINGS_COMMENT_TESTS ) 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 index d115bf08c..d9de6abdf 100644 --- 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 @@ -61,8 +61,14 @@ ] # Property [Query Shape Validation]: rejects malformed query shape documents -# with missing, empty, or null $db and unknown command types. +# 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: { @@ -124,7 +130,7 @@ ] # Property [Hash String Validation]: rejects invalid hash string formats -# including empty, too short, and non-hexadecimal strings. +# including empty, too short, too long, and non-hexadecimal strings. REMOVEQUERYSETTINGS_HASH_VALIDATION_TESTS: list[CommandTestCase] = [ CommandTestCase( "empty_hash_string", @@ -138,6 +144,12 @@ 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: { @@ -149,7 +161,8 @@ ), ] -# Property [Unrecognized Fields]: rejects unknown top-level command fields. +# 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", @@ -164,6 +177,19 @@ 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] = ( From ff45ff8eee6aeed7d74cc504c75d6fcb1651a790 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 16 Jun 2026 12:43:39 -0700 Subject: [PATCH 24/27] rename and remove duplicate Signed-off-by: Alina (Xi) Li --- .../test_removeQuerySettings_core.py | 190 +++++------------- 1 file changed, 51 insertions(+), 139 deletions(-) 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 index 41db5bc65..9dc0b6c6e 100644 --- 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 @@ -19,54 +19,11 @@ pytestmark = [pytest.mark.no_parallel] -# Property [Command Shape Acceptance]: removeQuerySettings accepts find, -# distinct, and aggregate query shapes. -REMOVEQUERYSETTINGS_COMMAND_SHAPE_TESTS: list[CommandTestCase] = [ +# Property [Find Shape Acceptance]: removeQuerySettings accepts find shapes +# with various field combinations without error. +REMOVEQUERYSETTINGS_FIND_ACCEPTANCE_TESTS: list[CommandTestCase] = [ CommandTestCase( - "find_shape", - command=lambda ctx: { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "sort": {"x": 1}, - "$db": ctx.database, - } - }, - expected={"ok": 1.0}, - msg="removeQuerySettings should accept valid find shape", - ), - CommandTestCase( - "distinct_shape", - command=lambda ctx: { - "removeQuerySettings": { - "distinct": ctx.collection, - "key": "x", - "query": {"x": {"$gt": 0}}, - "$db": ctx.database, - } - }, - expected={"ok": 1.0}, - msg="removeQuerySettings should accept valid distinct shape", - ), - CommandTestCase( - "aggregate_shape", - command=lambda ctx: { - "removeQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"x": 1}}], - "$db": ctx.database, - } - }, - expected={"ok": 1.0}, - msg="removeQuerySettings should accept valid aggregate shape", - ), -] - -# Property [Find Shape Variations]: removeQuerySettings accepts find shapes -# with various field combinations. -REMOVEQUERYSETTINGS_FIND_VARIATION_TESTS: list[CommandTestCase] = [ - CommandTestCase( - "find_filter_only", + "accepts_find_filter_only", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, @@ -78,7 +35,7 @@ msg="removeQuerySettings should accept find with filter only", ), CommandTestCase( - "find_filter_sort", + "accepts_find_filter_sort", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, @@ -91,7 +48,7 @@ msg="removeQuerySettings should accept find with filter and sort", ), CommandTestCase( - "find_filter_projection", + "accepts_find_filter_projection", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, @@ -104,7 +61,7 @@ msg="removeQuerySettings should accept find with filter and projection", ), CommandTestCase( - "find_filter_sort_projection", + "accepts_find_filter_sort_projection", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, @@ -118,7 +75,7 @@ msg="removeQuerySettings should accept find with all shape fields", ), CommandTestCase( - "find_with_collation", + "accepts_find_with_collation", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, @@ -131,7 +88,7 @@ msg="removeQuerySettings should accept find with collation", ), CommandTestCase( - "find_with_let", + "accepts_find_with_let", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, @@ -144,25 +101,23 @@ msg="removeQuerySettings should accept find with let", ), CommandTestCase( - "find_with_limit", + "accepts_find_without_filter", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, - "filter": {"g": 1}, - "limit": 10, "$db": ctx.database, } }, expected={"ok": 1.0}, - msg="removeQuerySettings should accept find with limit", + msg="removeQuerySettings should accept find without filter", ), ] -# Property [Distinct Shape Variations]: removeQuerySettings accepts distinct -# shapes with various field combinations. -REMOVEQUERYSETTINGS_DISTINCT_VARIATION_TESTS: list[CommandTestCase] = [ +# Property [Distinct Shape Acceptance]: removeQuerySettings accepts distinct +# shapes with various field combinations without error. +REMOVEQUERYSETTINGS_DISTINCT_ACCEPTANCE_TESTS: list[CommandTestCase] = [ CommandTestCase( - "distinct_key_only", + "accepts_distinct_key_only", command=lambda ctx: { "removeQuerySettings": { "distinct": ctx.collection, @@ -174,7 +129,7 @@ msg="removeQuerySettings should accept distinct with key only", ), CommandTestCase( - "distinct_complex_query", + "accepts_distinct_key_with_query", command=lambda ctx: { "removeQuerySettings": { "distinct": ctx.collection, @@ -184,29 +139,15 @@ } }, expected={"ok": 1.0}, - msg="removeQuerySettings should accept distinct with complex query", - ), - CommandTestCase( - "distinct_with_collation", - command=lambda ctx: { - "removeQuerySettings": { - "distinct": ctx.collection, - "key": "x", - "query": {"x": 1}, - "collation": {"locale": "en"}, - "$db": ctx.database, - } - }, - expected={"ok": 1.0}, - msg="removeQuerySettings should accept distinct with collation", + msg="removeQuerySettings should accept distinct with query filter", ), ] -# Property [Aggregate Shape Variations]: removeQuerySettings accepts aggregate -# pipeline shapes with various stage combinations. -REMOVEQUERYSETTINGS_AGGREGATE_VARIATION_TESTS: list[CommandTestCase] = [ +# Property [Aggregate Shape Acceptance]: removeQuerySettings accepts aggregate +# pipeline shapes with various stage combinations without error. +REMOVEQUERYSETTINGS_AGGREGATE_ACCEPTANCE_TESTS: list[CommandTestCase] = [ CommandTestCase( - "aggregate_match_only", + "accepts_aggregate_single_stage", command=lambda ctx: { "removeQuerySettings": { "aggregate": ctx.collection, @@ -215,10 +156,10 @@ } }, expected={"ok": 1.0}, - msg="removeQuerySettings should accept aggregate with $match only", + msg="removeQuerySettings should accept aggregate with single stage", ), CommandTestCase( - "aggregate_match_group", + "accepts_aggregate_multi_stage", command=lambda ctx: { "removeQuerySettings": { "aggregate": ctx.collection, @@ -230,31 +171,15 @@ } }, expected={"ok": 1.0}, - msg="removeQuerySettings should accept aggregate with $match and $group", - ), - CommandTestCase( - "aggregate_match_sort_limit", - command=lambda ctx: { - "removeQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [ - {"$match": {"n": 1}}, - {"$sort": {"n": 1}}, - {"$limit": 5}, - ], - "$db": ctx.database, - } - }, - expected={"ok": 1.0}, - msg="removeQuerySettings should accept aggregate with $match, $sort, and $limit", + msg="removeQuerySettings should accept aggregate with multiple stages", ), ] -# Property [$db Field Variations]: removeQuerySettings accepts non-existent -# and special-character database names in the $db field. -REMOVEQUERYSETTINGS_DB_VARIATION_TESTS: list[CommandTestCase] = [ +# Property [Nonexistent $db Acceptance]: removeQuerySettings accepts +# non-existent database names in the $db field without error. +REMOVEQUERYSETTINGS_DB_ACCEPTANCE_TESTS: list[CommandTestCase] = [ CommandTestCase( - "db_nonexistent", + "accepts_nonexistent_db", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, @@ -265,25 +190,13 @@ expected={"ok": 1.0}, msg="removeQuerySettings should accept non-existent $db", ), - CommandTestCase( - "db_special_characters", - command=lambda ctx: { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"p": 1}, - "$db": "test-special-db", - } - }, - expected={"ok": 1.0}, - msg="removeQuerySettings should accept $db with special characters", - ), ] -# Property [Idempotent Removal]: removeQuerySettings succeeds silently when +# Property [Silent No-Op]: removeQuerySettings succeeds silently when # no matching settings exist. -REMOVEQUERYSETTINGS_IDEMPOTENT_TESTS: list[CommandTestCase] = [ +REMOVEQUERYSETTINGS_NOOP_TESTS: list[CommandTestCase] = [ CommandTestCase( - "nonexistent_query_shape", + "noop_nonexistent_query_shape", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, @@ -295,7 +208,7 @@ msg="removeQuerySettings should succeed when no matching settings exist", ), CommandTestCase( - "nonexistent_hash", + "noop_nonexistent_hash", command=lambda ctx: { "removeQuerySettings": "00000000000000000000000000000000" "00000000000000000000000000000000" @@ -304,21 +217,21 @@ msg="removeQuerySettings should succeed with a non-existent hash", ), CommandTestCase( - "lowercase_hash", + "noop_lowercase_hash", command=lambda ctx: { "removeQuerySettings": "abcdef0123456789abcdef0123456789" "abcdef0123456789abcdef0123456789" }, expected={"ok": 1.0}, - msg="removeQuerySettings should accept lowercase hex hash string", + msg="removeQuerySettings should accept lowercase hex hash", ), ] -# Property [IDHACK Query Acceptance]: removeQuerySettings accepts -# IDHACK-eligible queries without error. +# Property [IDHACK Query Acceptance]: unlike setQuerySettings which rejects +# IDHACK-eligible queries, removeQuerySettings accepts them. REMOVEQUERYSETTINGS_IDHACK_TESTS: list[CommandTestCase] = [ CommandTestCase( - "idhack_query", + "accepts_idhack_query", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, @@ -331,11 +244,11 @@ ), ] -# Property [System Collection Acceptance]: removeQuerySettings accepts query -# shapes targeting system collections and internal databases. -REMOVEQUERYSETTINGS_SYSTEM_TESTS: list[CommandTestCase] = [ +# Property [Internal Database Acceptance]: unlike setQuerySettings which +# rejects internal databases, removeQuerySettings accepts them. +REMOVEQUERYSETTINGS_INTERNAL_DB_TESTS: list[CommandTestCase] = [ CommandTestCase( - "system_collection", + "accepts_admin_db", command=lambda ctx: { "removeQuerySettings": { "find": "system.users", @@ -344,10 +257,10 @@ } }, expected={"ok": 1.0}, - msg="removeQuerySettings should accept system collection query shapes", + msg="removeQuerySettings should accept admin database query shapes", ), CommandTestCase( - "local_database", + "accepts_local_db", command=lambda ctx: { "removeQuerySettings": { "find": "oplog.rs", @@ -361,10 +274,10 @@ ] # Property [Comment Field Acceptance]: removeQuerySettings accepts the -# comment field without error. +# comment top-level field without error. REMOVEQUERYSETTINGS_COMMENT_TESTS: list[CommandTestCase] = [ CommandTestCase( - "comment_field_accepted", + "accepts_comment_field", command=lambda ctx: { "removeQuerySettings": { "find": ctx.collection, @@ -379,14 +292,13 @@ ] REMOVEQUERYSETTINGS_CORE_TESTS: list[CommandTestCase] = ( - REMOVEQUERYSETTINGS_COMMAND_SHAPE_TESTS - + REMOVEQUERYSETTINGS_FIND_VARIATION_TESTS - + REMOVEQUERYSETTINGS_DISTINCT_VARIATION_TESTS - + REMOVEQUERYSETTINGS_AGGREGATE_VARIATION_TESTS - + REMOVEQUERYSETTINGS_DB_VARIATION_TESTS - + REMOVEQUERYSETTINGS_IDEMPOTENT_TESTS + REMOVEQUERYSETTINGS_FIND_ACCEPTANCE_TESTS + + REMOVEQUERYSETTINGS_DISTINCT_ACCEPTANCE_TESTS + + REMOVEQUERYSETTINGS_AGGREGATE_ACCEPTANCE_TESTS + + REMOVEQUERYSETTINGS_DB_ACCEPTANCE_TESTS + + REMOVEQUERYSETTINGS_NOOP_TESTS + REMOVEQUERYSETTINGS_IDHACK_TESTS - + REMOVEQUERYSETTINGS_SYSTEM_TESTS + + REMOVEQUERYSETTINGS_INTERNAL_DB_TESTS + REMOVEQUERYSETTINGS_COMMENT_TESTS ) From 3c6e98c245967341dc5a6170da288e349b075d5d Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 16 Jun 2026 12:58:16 -0700 Subject: [PATCH 25/27] merge same test functions into 1 Signed-off-by: Alina (Xi) Li --- .../test_removeQuerySettings_behavior.py | 121 +++++++----------- 1 file changed, 44 insertions(+), 77 deletions(-) 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 index fe88786b4..18cc6ddd2 100644 --- 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 @@ -31,7 +31,10 @@ # distinct query shapes, verified via $querySettings. # Property [Remove Aggregate Shape]: removeQuerySettings removes settings for # aggregate query shapes, verified via $querySettings. -REMOVEQUERYSETTINGS_VERIFIED_REMOVAL_TESTS: list[SettingsTestCase] = [ +# 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: [ @@ -178,13 +181,51 @@ ], 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.replica_set -@pytest.mark.parametrize("test", pytest_params(REMOVEQUERYSETTINGS_VERIFIED_REMOVAL_TESTS)) -def test_removeQuerySettings_verified_removal(collection, test): +@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: @@ -285,80 +326,6 @@ def test_removeQuerySettings_idempotent(collection, test): pass -# Property [Shape Matching Ignores Filter Values]: query shape matching uses -# field structure, not values. Removing with different filter values removes -# the original setting. -REMOVEQUERYSETTINGS_SHAPE_REMOVES_TESTS: list[SettingsTestCase] = [ - 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.replica_set -@pytest.mark.parametrize("test", pytest_params(REMOVEQUERYSETTINGS_SHAPE_REMOVES_TESTS)) -def test_removeQuerySettings_shape_removes(collection, test): - """Test that shape matching ignores filter values — same shape is removed.""" - 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 [Shape Matching Includes Collection]: collection name is part of # the query shape. Removing with a different collection does not affect the # original setting. From 0e61e56a4018832a23bc40f98949ef0dda6dd774 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 16 Jun 2026 13:29:02 -0700 Subject: [PATCH 26/27] delete setQuerySettings files Signed-off-by: Alina (Xi) Li --- .../commands/setQuerySettings/__init__.py | 0 .../test_setQuerySettings_behavior.py | 376 ------- .../test_setQuerySettings_query_shapes.py | 601 ----------- .../test_setQuerySettings_reject.py | 189 ---- .../test_setQuerySettings_reject_errors.py | 134 --- .../test_setQuerySettings_settings.py | 785 -------------- .../test_setQuerySettings_type_errors.py | 308 ------ ...test_setQuerySettings_validation_errors.py | 426 -------- .../test_setQuerySettings_verification.py | 970 ------------------ .../setQuerySettings/utils/__init__.py | 0 .../utils/setQuerySettings_common.py | 15 - 11 files changed, 3804 deletions(-) delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/__init__.py delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject_errors.py delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/__init__.py delete mode 100644 documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/setQuerySettings_common.py diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/__init__.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py deleted file mode 100644 index c3353a7c8..000000000 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_behavior.py +++ /dev/null @@ -1,376 +0,0 @@ -"""Tests for setQuerySettings command behavioral verification. - -Validates that query settings are retrievable via $querySettings aggregation -stage, removable via removeQuerySettings, and that the response structure -includes expected fields like queryShapeHash and representativeQuery. -""" - -from __future__ import annotations - -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 -from documentdb_tests.framework.parametrize import pytest_params - -from .utils.setQuerySettings_common import get_query_settings - -# Property [Response Structure]: setQuerySettings response includes hash, query, and settings. -SET_QUERY_SETTINGS_RESPONSE_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "response_contains_hash", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"b1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"b1": 1}, - "$db": ctx.database, - } - } - ], - msg="response should contain queryShapeHash", - ), - SettingsTestCase( - "response_contains_representative_query", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"b2": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"b2": 1}, - "$db": ctx.database, - } - } - ], - msg="response should contain representativeQuery", - ), - SettingsTestCase( - "response_settings_echo", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"b3": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected=lambda ctx: { - "ok": 1.0, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"b3": 1}, - "$db": ctx.database, - } - } - ], - msg="response should echo applied settings", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_RESPONSE_TESTS)) -def test_setQuerySettings_response(collection, test): - """Test setQuerySettings response structure.""" - ctx = CommandContext.from_collection(collection) - try: - result = execute_admin_command(collection, test.build_command(ctx)) - expected = test.build_expected(ctx) - # Also verify the dynamic fields are present - if test.id == "response_contains_hash": - expected["queryShapeHash"] = result.get("queryShapeHash") - elif test.id == "response_contains_representative_query": - expected["representativeQuery"] = result.get("representativeQuery") - assertSuccessPartial(result, expected, msg=test.msg) - finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass - - -# Property [removeQuerySettings]: settings can be removed by query or hash. -SET_QUERY_SETTINGS_REMOVE_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "removeQuerySettings_by_query", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"b5": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - ], - command=lambda ctx: { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"b5": 1}, - "$db": ctx.database, - } - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"b5": 1}, - "$db": ctx.database, - } - } - ], - msg="removeQuerySettings by query should succeed", - ), - SettingsTestCase( - "removeQuerySettings_by_hash", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"b6": 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": {"b6": 1}, - "$db": ctx.database, - } - } - ], - msg="removeQuerySettings by hash should succeed", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_REMOVE_TESTS)) -def test_setQuerySettings_remove(collection, test): - """Test removeQuerySettings removes 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) - 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 [$querySettings Retrieval]: settings are visible via $querySettings aggregation stage. -SET_QUERY_SETTINGS_QS_STAGE_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "querySettings_stage_retrieval", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"b4": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - ], - expected=lambda ctx: {"queryShapeHash": ctx.setup_results[0]["queryShapeHash"]}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"b4": 1}, - "$db": ctx.database, - } - } - ], - msg="$querySettings should return the created setting", - ), - SettingsTestCase( - "querySettings_stage_shows_settings", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"b9": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - ], - expected=lambda ctx: { - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"b9": 1}, - "$db": ctx.database, - } - } - ], - msg="$querySettings should include indexHints in settings", - ), - SettingsTestCase( - "querySettings_stage_shows_representative_query", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"b10": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - ], - # expected is built dynamically in the runner (self-referential) - expected=None, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"b10": 1}, - "$db": ctx.database, - } - } - ], - msg="$querySettings should include representativeQuery", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_QS_STAGE_TESTS)) -def test_setQuerySettings_qs_stage(collection, test): - """Test that settings are visible via $querySettings aggregation stage.""" - 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"] - settings = get_query_settings(collection) - matching = [s for s in settings if s.get("queryShapeHash") == expected_hash] - entry = matching[0] if matching else {} - expected = test.build_expected(ctx) - if expected is None: - # Self-referential: verify the field exists - expected = {"representativeQuery": entry.get("representativeQuery")} - assertSuccessPartial(entry, expected, 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/setQuerySettings/test_setQuerySettings_query_shapes.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py deleted file mode 100644 index 9a975aef8..000000000 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_query_shapes.py +++ /dev/null @@ -1,601 +0,0 @@ -"""Tests for setQuerySettings command query shape acceptance. - -Validates that the setQuerySettings command accepts valid query shapes for -find, distinct, and aggregate commands, including various shape variations, -field combinations, and $db field variations. -""" - -from __future__ import annotations - -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 -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Command Shape Acceptance]: accepts find, distinct, and aggregate shapes. -# Property [Find Shape Variations]: setQuerySettings accepts find shapes with various field combos. -# Property [Distinct Shape Variations]: setQuerySettings accepts distinct shapes with query combos. -# Property [Aggregate Shape Variations]: setQuerySettings accepts aggregate pipeline shapes. -# Property [$db Field Variations]: setQuerySettings accepts non-existent and special-char db names. -SET_QUERY_SETTINGS_QUERY_SHAPE_TESTS: list[SettingsTestCase] = [ - # -- Command shape acceptance -- - SettingsTestCase( - "find_shape", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "sort": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "sort": {"x": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept valid find shape", - ), - SettingsTestCase( - "distinct_shape", - command=lambda ctx: { - "setQuerySettings": { - "distinct": ctx.collection, - "key": "x", - "query": {"x": {"$gt": 0}}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "distinct": ctx.collection, - "key": "x", - "query": {"x": {"$gt": 0}}, - "$db": ctx.database, - } - } - ], - msg="should accept valid distinct shape", - ), - SettingsTestCase( - "aggregate_shape", - command=lambda ctx: { - "setQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"x": 1}}], - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"x": 1}}], - "$db": ctx.database, - } - } - ], - msg="should accept valid aggregate shape", - ), - # -- Find shape variations -- - SettingsTestCase( - "find_filter_only", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept find with filter only", - ), - SettingsTestCase( - "find_filter_sort", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"b": 1}, - "sort": {"b": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"b": 1}, - "sort": {"b": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept find with filter+sort", - ), - SettingsTestCase( - "find_filter_projection", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"c": 1}, - "projection": {"c": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"c": 1}, - "projection": {"c": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept find with filter+projection", - ), - SettingsTestCase( - "find_filter_sort_projection", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"d": 1}, - "sort": {"d": 1}, - "projection": {"d": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"d": 1}, - "sort": {"d": 1}, - "projection": {"d": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept find with all fields", - ), - SettingsTestCase( - "find_with_collation", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"e": "abc"}, - "collation": {"locale": "en", "strength": 2}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"e": "abc"}, - "collation": {"locale": "en", "strength": 2}, - "$db": ctx.database, - } - } - ], - msg="should accept find with collation", - ), - SettingsTestCase( - "find_with_let", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"$expr": {"$eq": ["$f", "$$target"]}}, - "let": {"target": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"$expr": {"$eq": ["$f", "$$target"]}}, - "let": {"target": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept find with let", - ), - SettingsTestCase( - "find_with_limit", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"g": 1}, - "limit": 10, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"g": 1}, - "limit": 10, - "$db": ctx.database, - } - } - ], - msg="should accept find with limit", - ), - # -- Distinct shape variations -- - SettingsTestCase( - "distinct_key_only", - command=lambda ctx: { - "setQuerySettings": { - "distinct": ctx.collection, - "key": "j", - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "distinct": ctx.collection, - "key": "j", - "$db": ctx.database, - } - } - ], - msg="should accept distinct key only", - ), - SettingsTestCase( - "distinct_complex_query", - command=lambda ctx: { - "setQuerySettings": { - "distinct": ctx.collection, - "key": "k", - "query": {"$and": [{"k": {"$gt": 0}}, {"k": {"$lt": 100}}]}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "distinct": ctx.collection, - "key": "k", - "query": {"$and": [{"k": {"$gt": 0}}, {"k": {"$lt": 100}}]}, - "$db": ctx.database, - } - } - ], - msg="should accept distinct complex query", - ), - # -- Aggregate shape variations -- - SettingsTestCase( - "aggregate_match_only", - command=lambda ctx: { - "setQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"l": 1}}], - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"l": 1}}], - "$db": ctx.database, - } - } - ], - msg="should accept aggregate $match only", - ), - SettingsTestCase( - "aggregate_match_group", - command=lambda ctx: { - "setQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [ - {"$match": {"m": 1}}, - {"$group": {"_id": "$m", "count": {"$sum": 1}}}, - ], - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [ - {"$match": {"m": 1}}, - {"$group": {"_id": "$m", "count": {"$sum": 1}}}, - ], - "$db": ctx.database, - } - } - ], - msg="should accept aggregate $match+$group", - ), - SettingsTestCase( - "aggregate_match_sort_limit", - command=lambda ctx: { - "setQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"n": 1}}, {"$sort": {"n": 1}}, {"$limit": 5}], - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"n": 1}}, {"$sort": {"n": 1}}, {"$limit": 5}], - "$db": ctx.database, - } - } - ], - msg="should accept aggregate $match+$sort+$limit", - ), - SettingsTestCase( - "aggregate_empty_pipeline", - command=lambda ctx: { - "setQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [], - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [], - "$db": ctx.database, - } - } - ], - msg="should accept aggregate with empty pipeline", - ), - # -- $db field variations -- - SettingsTestCase( - "db_nonexistent", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"o": 1}, - "$db": "nonexistent_db_for_query_settings_test", - }, - "settings": { - "indexHints": [ - { - "ns": { - "db": "nonexistent_db_for_query_settings_test", - "coll": ctx.collection, - }, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"o": 1}, - "$db": "nonexistent_db_for_query_settings_test", - } - } - ], - msg="should accept non-existent $db", - ), - SettingsTestCase( - "db_special_characters", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"p": 1}, - "$db": "test-special-db", - }, - "settings": { - "indexHints": [ - { - "ns": {"db": "test-special-db", "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"p": 1}, - "$db": "test-special-db", - } - } - ], - msg="should accept $db with special chars", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_QUERY_SHAPE_TESTS)) -def test_setQuerySettings_query_shapes(collection, test): - """Test setQuerySettings accepts valid query shapes.""" - ctx = CommandContext.from_collection(collection) - try: - 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 diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py deleted file mode 100644 index 5b4d2d16e..000000000 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject.py +++ /dev/null @@ -1,189 +0,0 @@ -"""Tests for setQuerySettings reject field success behavior. - -Validates that rejection does not affect unrelated query shapes, -and that reject can be reversed via update or removal. -""" - -from __future__ import annotations - -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 - -# Property [Reject Scope]: reject: true does not affect unrelated query shapes. -# Property [Reject Reversal via Update]: updating reject to false re-enables the query. -# Property [Reject Reversal via Remove]: removing the query setting re-enables the query. -# Property [Reject False Succeeds]: reject: false with indexHints allows the query. -SET_QUERY_SETTINGS_REJECT_SUCCESS_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "reject_does_not_affect_different_shape", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"rej_s1": 1}, - "$db": ctx.database, - }, - "settings": {"reject": True}, - } - ], - command=lambda ctx: { - "find": ctx.collection, - "filter": {"rej_s2": 1}, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"rej_s1": 1}, - "$db": ctx.database, - } - } - ], - msg="different query shape should not be rejected", - ), - SettingsTestCase( - "reject_reversed_by_update", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"rej_u1": 1}, - "$db": ctx.database, - }, - "settings": {"reject": True}, - }, - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"rej_u1": 1}, - "$db": ctx.database, - }, - "settings": { - "reject": False, - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - ], - command=lambda ctx: { - "find": ctx.collection, - "filter": {"rej_u1": 1}, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"rej_u1": 1}, - "$db": ctx.database, - } - } - ], - msg="query should succeed after reject updated to false", - ), - SettingsTestCase( - "reject_reversed_by_remove", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"rej_r1": 1}, - "$db": ctx.database, - }, - "settings": {"reject": True}, - }, - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"rej_r1": 1}, - "$db": ctx.database, - } - }, - ], - command=lambda ctx: { - "find": ctx.collection, - "filter": {"rej_r1": 1}, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"rej_r1": 1}, - "$db": ctx.database, - } - } - ], - msg="query should succeed after removeQuerySettings", - ), - SettingsTestCase( - "reject_false_allows_query", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"rej_f1": 1}, - "$db": ctx.database, - }, - "settings": { - "reject": False, - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - ], - command=lambda ctx: { - "find": ctx.collection, - "filter": {"rej_f1": 1}, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"rej_f1": 1}, - "$db": ctx.database, - } - } - ], - msg="query with reject: false should succeed", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_REJECT_SUCCESS_TESTS)) -def test_setQuerySettings_reject_success(collection, test): - """Test that reject scope and reversal work correctly.""" - 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_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 diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject_errors.py deleted file mode 100644 index ff0605fb4..000000000 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_reject_errors.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Tests for setQuerySettings reject field error behavior. - -Validates that reject: true blocks matching queries for find, distinct, and -aggregate commands at execution time. -""" - -from __future__ import annotations - -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 assertResult -from documentdb_tests.framework.error_codes import QUERYSETTINGS_QUERY_REJECTED_ERROR -from documentdb_tests.framework.executor import execute_admin_command, execute_command -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Reject Blocks Find]: reject: true blocks matching find queries. -# Property [Reject Blocks Distinct]: reject: true blocks matching distinct queries. -# Property [Reject Blocks Aggregate]: reject: true blocks matching aggregate queries. -SET_QUERY_SETTINGS_REJECT_ERROR_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "reject_blocks_find", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"b8": 1}, - "$db": ctx.database, - }, - "settings": {"reject": True}, - } - ], - command=lambda ctx: { - "find": ctx.collection, - "filter": {"b8": 1}, - }, - error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"b8": 1}, - "$db": ctx.database, - } - } - ], - msg="query matching reject: true setting should be rejected", - ), - SettingsTestCase( - "reject_blocks_distinct", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "distinct": ctx.collection, - "key": "x", - "query": {"rej_d1": 1}, - "$db": ctx.database, - }, - "settings": {"reject": True}, - } - ], - command=lambda ctx: { - "distinct": ctx.collection, - "key": "x", - "query": {"rej_d1": 1}, - }, - error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "distinct": ctx.collection, - "key": "x", - "query": {"rej_d1": 1}, - "$db": ctx.database, - } - } - ], - msg="distinct query matching reject: true should be rejected", - ), - SettingsTestCase( - "reject_blocks_aggregate", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"rej_a1": 1}}], - "$db": ctx.database, - }, - "settings": {"reject": True}, - } - ], - command=lambda ctx: { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"rej_a1": 1}}], - "cursor": {}, - }, - error_code=QUERYSETTINGS_QUERY_REJECTED_ERROR, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"rej_a1": 1}}], - "$db": ctx.database, - } - } - ], - msg="aggregate query matching reject: true should be rejected", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_REJECT_ERROR_TESTS)) -def test_setQuerySettings_reject_errors(collection, test): - """Test that reject: true blocks matching queries.""" - 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_command(collection, test.build_command(ctx)) - assertResult(result, error_code=test.error_code, 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/setQuerySettings/test_setQuerySettings_settings.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py deleted file mode 100644 index f5c46ff44..000000000 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_settings.py +++ /dev/null @@ -1,785 +0,0 @@ -"""Tests for setQuerySettings command settings configurations. - -Validates that the setQuerySettings command accepts valid settings -combinations including indexHints, reject, queryFramework, and comment -fields, as well as allowedIndexes variations and update behavior. -""" - -from __future__ import annotations - -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 -from documentdb_tests.framework.parametrize import pytest_params - -# Property [indexHints Acceptance]: setQuerySettings accepts valid indexHints configurations. -# Property [reject Acceptance]: setQuerySettings accepts reject: true alone or with indexHints. -# Property [queryFramework Acceptance]: setQuerySettings accepts classic and sbe frameworks. -# Property [comment Acceptance]: setQuerySettings accepts comment as any BSON type. -# Property [Combined Settings]: setQuerySettings accepts all settings fields together. -# Property [$natural Hint]: setQuerySettings accepts $natural in allowedIndexes. -# Property [Multiple indexHints]: setQuerySettings accepts multiple indexHints documents. -# Property [Non-Existent Index]: setQuerySettings accepts non-existent index names. -# Property [Text Index Spec]: setQuerySettings accepts text index key pattern in allowedIndexes. -# Property [2dsphere Index Spec]: setQuerySettings accepts 2dsphere index key pattern. -# Property [2d Index Spec]: setQuerySettings accepts 2d index key pattern. -# Property [Hashed Index Spec]: setQuerySettings accepts hashed index key pattern. -SET_QUERY_SETTINGS_SETTINGS_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "indexHints_single_index", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a1": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept indexHints with single index", - ), - SettingsTestCase( - "indexHints_multiple_indexes", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a2": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_", {"a2": 1}], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a2": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept multiple indexes", - ), - SettingsTestCase( - "indexHints_key_pattern", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a3": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": [{"a3": 1}], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a3": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept indexHints with key pattern", - ), - SettingsTestCase( - "reject_true", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a5": 1}, - "$db": ctx.database, - }, - "settings": {"reject": True}, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a5": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept settings with reject: true", - ), - SettingsTestCase( - "reject_with_indexHints", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a6": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "reject": True, - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a6": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept reject with indexHints", - ), - SettingsTestCase( - "queryFramework_classic", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a7": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": "classic", - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a7": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept queryFramework: classic", - ), - SettingsTestCase( - "queryFramework_sbe", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a8": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": "sbe", - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a8": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept queryFramework: sbe", - ), - SettingsTestCase( - "with_comment_string", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a9": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "comment": "test comment for setQuerySettings", - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a9": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept settings with comment string", - ), - SettingsTestCase( - "all_settings_combined", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a12": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": "classic", - "reject": True, - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a12": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept all settings combined", - ), - SettingsTestCase( - "indexHints_natural", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a13": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["$natural"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a13": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept $natural in allowedIndexes", - ), - SettingsTestCase( - "indexHints_multiple_ns_documents", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a14": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - }, - { - "ns": {"db": ctx.database, "coll": "other_collection"}, - "allowedIndexes": ["_id_"], - }, - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a14": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept multiple indexHints documents", - ), - SettingsTestCase( - "indexHints_nonexistent_index", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a15": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["nonexistent_index"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a15": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept non-existent index name", - ), - SettingsTestCase( - "comment_object", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a16": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "comment": {"body": {"msg": "Updated"}}, - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a16": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept settings with comment as object", - ), - SettingsTestCase( - "comment_int", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a17": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "comment": 42, - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a17": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept settings with comment as int", - ), - SettingsTestCase( - "comment_bool", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a18": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "comment": True, - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a18": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept settings with comment as bool", - ), - SettingsTestCase( - "comment_array", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a19": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "comment": ["tag1", "tag2"], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a19": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept settings with comment as array", - ), - SettingsTestCase( - "comment_null", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a20": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "comment": None, - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a20": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept settings with comment as null", - ), - SettingsTestCase( - "indexHints_text_index_spec", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a21": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": [{"a21": "text"}], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a21": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept text index key pattern in allowedIndexes", - ), - SettingsTestCase( - "indexHints_2dsphere_index_spec", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a22": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": [{"geo": "2dsphere"}], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a22": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept 2dsphere index key pattern in allowedIndexes", - ), - SettingsTestCase( - "indexHints_2d_index_spec", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a23": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": [{"loc": "2d"}], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a23": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept 2d index key pattern in allowedIndexes", - ), - SettingsTestCase( - "indexHints_hashed_index_spec", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a24": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": [{"a24": "hashed"}], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a24": 1}, - "$db": ctx.database, - } - } - ], - msg="should accept hashed index key pattern in allowedIndexes", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_SETTINGS_TESTS)) -def test_setQuerySettings_settings(collection, test): - """Test setQuerySettings accepts valid settings configurations.""" - 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 [Update Behavior]: setQuerySettings can update existing settings by query or hash. -SET_QUERY_SETTINGS_UPDATE_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "update_existing_settings", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a10": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - ], - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a10": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_", {"a10": 1}], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a10": 1}, - "$db": ctx.database, - } - } - ], - msg="update setQuerySettings should succeed", - ), - SettingsTestCase( - "update_via_hash", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a11": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - ], - command=lambda ctx: { - "setQuerySettings": ctx.setup_results[0]["queryShapeHash"], - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_", {"a11": 1}], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"a11": 1}, - "$db": ctx.database, - } - } - ], - msg="update via hash should succeed", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_UPDATE_TESTS)) -def test_setQuerySettings_update(collection, test): - """Test setQuerySettings can update existing settings by query or hash.""" - 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 diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py deleted file mode 100644 index 2c9d0304d..000000000 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_type_errors.py +++ /dev/null @@ -1,308 +0,0 @@ -"""Tests for setQuerySettings command BSON type rejection. - -Validates that the setQuerySettings command rejects invalid BSON types for -the primary argument field, the queryFramework sub-field, the reject sub-field, -and the indexHints namespace and allowedIndexes sub-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 ( - FAILED_TO_PARSE_ERROR, - MISSING_FIELD_ERROR, - TYPE_MISMATCH_ERROR, -) -from documentdb_tests.framework.executor import execute_admin_command -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Primary Argument Type Rejection]: the setQuerySettings field must -# be a document (query shape) or string (hash). All other BSON types are -# rejected with TYPE_MISMATCH_ERROR. -SET_QUERY_SETTINGS_PRIMARY_ARG_TYPE_TESTS: list[CommandTestCase] = [ - CommandTestCase( - f"primary_arg_{tid}", - command=lambda ctx, v=value: { - "setQuerySettings": v, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=TYPE_MISMATCH_ERROR, - msg=f"setQuerySettings 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 [queryFramework Type Rejection]: the queryFramework field must be a -# string. Non-string BSON types are rejected with TYPE_MISMATCH_ERROR. -SET_QUERY_SETTINGS_QUERY_FRAMEWORK_TYPE_TESTS: list[CommandTestCase] = [ - CommandTestCase( - f"query_framework_{tid}", - command=lambda ctx, v=value: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": v, - }, - }, - error_code=TYPE_MISMATCH_ERROR, - msg=f"setQuerySettings should reject {tid} as queryFramework", - ) - for tid, value in [ - ("int32", 42), - ("int64", Int64(42)), - ("double", 3.14), - ("decimal128", Decimal128("1")), - ("bool_true", True), - ("bool_false", False), - ("array", [1]), - ("object", {"k": "v"}), - ("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 [reject Type Rejection]: the reject field must be a boolean. -# Non-boolean BSON types are rejected with TYPE_MISMATCH_ERROR. -SET_QUERY_SETTINGS_REJECT_TYPE_TESTS: list[CommandTestCase] = [ - CommandTestCase( - f"reject_{tid}", - command=lambda ctx, v=value: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "reject": v, - }, - }, - error_code=TYPE_MISMATCH_ERROR, - msg=f"setQuerySettings should reject {tid} as reject field", - ) - for tid, value in [ - ("null", None), - ("int32", 42), - ("int64", Int64(42)), - ("double", 3.14), - ("decimal128", Decimal128("1")), - ("string", "true"), - ("array", [True]), - ("object", {"k": "v"}), - ("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 [indexHints.ns.db Type Rejection]: the ns.db field must be a string. -SET_QUERY_SETTINGS_NS_DB_TYPE_TESTS: list[CommandTestCase] = [ - CommandTestCase( - f"ns_db_{tid}", - command=lambda ctx, v=value: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": v, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=TYPE_MISMATCH_ERROR, - msg=f"setQuerySettings should reject {tid} as indexHints.ns.db", - ) - for tid, value in [ - ("int32", 42), - ("bool", True), - ("array", ["test"]), - ("object", {"k": "v"}), - ] -] - -# Property [indexHints.ns.coll Type Rejection]: the ns.coll field must be a string. -SET_QUERY_SETTINGS_NS_COLL_TYPE_TESTS: list[CommandTestCase] = [ - CommandTestCase( - f"ns_coll_{tid}", - command=lambda ctx, v=value: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": v}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=TYPE_MISMATCH_ERROR, - msg=f"setQuerySettings should reject {tid} as indexHints.ns.coll", - ) - for tid, value in [ - ("int32", 42), - ("bool", True), - ] -] - -# Property [indexHints.allowedIndexes Type Rejection]: allowedIndexes must be an array. -SET_QUERY_SETTINGS_ALLOWED_INDEXES_TYPE_TESTS: list[CommandTestCase] = [ - CommandTestCase( - f"allowed_indexes_{tid}", - command=lambda ctx, v=value: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": v, - } - ], - }, - }, - error_code=TYPE_MISMATCH_ERROR, - msg=f"setQuerySettings should reject {tid} as indexHints.allowedIndexes", - ) - for tid, value in [ - ("string", "_id_"), - ("int32", 42), - ] -] - -# Property [allowedIndexes null]: null allowedIndexes treated as missing required field. -SET_QUERY_SETTINGS_ALLOWED_INDEXES_EDGE_TESTS: list[CommandTestCase] = [ - CommandTestCase( - "allowed_indexes_null_missing", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": None, - } - ], - }, - }, - error_code=MISSING_FIELD_ERROR, - msg="setQuerySettings should reject null allowedIndexes as missing field", - ), - CommandTestCase( - "allowed_indexes_non_string_element", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": [42], - } - ], - }, - }, - error_code=FAILED_TO_PARSE_ERROR, - msg="setQuerySettings should reject non-string elements in allowedIndexes", - ), -] - -SET_QUERY_SETTINGS_TYPE_ERROR_TESTS: list[CommandTestCase] = ( - SET_QUERY_SETTINGS_PRIMARY_ARG_TYPE_TESTS - + SET_QUERY_SETTINGS_QUERY_FRAMEWORK_TYPE_TESTS - + SET_QUERY_SETTINGS_REJECT_TYPE_TESTS - + SET_QUERY_SETTINGS_NS_DB_TYPE_TESTS - + SET_QUERY_SETTINGS_NS_COLL_TYPE_TESTS - + SET_QUERY_SETTINGS_ALLOWED_INDEXES_TYPE_TESTS - + SET_QUERY_SETTINGS_ALLOWED_INDEXES_EDGE_TESTS -) - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_TYPE_ERROR_TESTS)) -def test_setQuerySettings_type_errors(collection, test): - """Test setQuerySettings BSON type rejection.""" - ctx = CommandContext.from_collection(collection) - result = execute_admin_command(collection, test.build_command(ctx)) - assertResult( - result, - error_code=test.error_code, - msg=test.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py deleted file mode 100644 index cead12f5f..000000000 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_validation_errors.py +++ /dev/null @@ -1,426 +0,0 @@ -"""Tests for setQuerySettings command structural and validation errors. - -Validates that the setQuerySettings command rejects malformed query shapes, -invalid hash strings, missing or empty settings, unrecognized fields, invalid -queryFramework values, system collection restrictions, and that reject: true -blocks matching queries at execution time. -""" - -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 assertResult -from documentdb_tests.framework.error_codes import ( - BAD_VALUE_ERROR, - INVALID_LENGTH_ERROR, - INVALID_NAMESPACE_ERROR, - MISSING_FIELD_ERROR, - QUERYSETTINGS_EMPTY_SETTINGS_ERROR, - QUERYSETTINGS_IDHACK_QUERY_ERROR, - QUERYSETTINGS_INTERNAL_DB_ERROR, - QUERYSETTINGS_NS_COLL_MISSING_ERROR, - QUERYSETTINGS_NS_DB_MISSING_ERROR, - QUERYSETTINGS_REJECT_ONLY_ERROR, - QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, - UNRECOGNIZED_COMMAND_FIELD_ERROR, -) -from documentdb_tests.framework.executor import execute_admin_command -from documentdb_tests.framework.parametrize import pytest_params - -# Property [Query Shape Validation]: rejects malformed or unknown query shape documents. -# Property [Hash String Validation]: rejects invalid hash string formats. -# Property [indexHints Structure Validation]: rejects indexHints missing required sub-fields. -# Property [Settings Value Validation]: rejects invalid field values in settings document. -# Property [Settings Presence]: rejects missing or empty settings document. -# Property [Unrecognized Fields]: rejects unknown top-level command fields. -# Property [Database Restrictions]: rejects query shapes targeting internal databases. -# Property [indexHints Value Validation]: rejects empty allowedIndexes and IDHACK queries. -# Property [Reject Blocks Query]: a rejected query returns an error when executed. -SET_QUERY_SETTINGS_VALIDATION_ERROR_TESTS: list[CommandTestCase] = [ - CommandTestCase( - "query_shape_missing_db", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=MISSING_FIELD_ERROR, - msg="setQuerySettings should reject query shape missing $db field", - ), - CommandTestCase( - "query_shape_empty_db", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": "", - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=INVALID_NAMESPACE_ERROR, - msg="setQuerySettings should reject query shape with empty $db", - ), - CommandTestCase( - "query_shape_unknown_command", - command=lambda ctx: { - "setQuerySettings": { - "unknownCommand": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR, - msg="setQuerySettings should reject unknown command type in query shape", - ), - CommandTestCase( - "empty_hash_string", - command=lambda ctx: { - "setQuerySettings": "", - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=INVALID_LENGTH_ERROR, - msg="setQuerySettings should reject empty hash string", - ), - CommandTestCase( - "short_hash_string", - command=lambda ctx: { - "setQuerySettings": "tooshort", - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=BAD_VALUE_ERROR, - msg="setQuerySettings should reject hash string with wrong length", - ), - CommandTestCase( - "nonhex_hash_string", - command=lambda ctx: { - "setQuerySettings": "ZZZZZZZZ34567890ABCDEF1234567890" - "ABCDEF1234567890ABCDEF1234567890", - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=BAD_VALUE_ERROR, - msg="setQuerySettings should reject hash string with non-hex chars", - ), - CommandTestCase( - "indexHints_missing_ns", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=MISSING_FIELD_ERROR, - msg="setQuerySettings should reject indexHints missing ns field", - ), - CommandTestCase( - "indexHints_ns_missing_db", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=QUERYSETTINGS_NS_DB_MISSING_ERROR, - msg="setQuerySettings should reject indexHints.ns missing db field", - ), - CommandTestCase( - "indexHints_ns_null_db", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": None, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=QUERYSETTINGS_NS_DB_MISSING_ERROR, - msg="setQuerySettings should reject indexHints.ns with null db", - ), - CommandTestCase( - "indexHints_ns_missing_coll", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=QUERYSETTINGS_NS_COLL_MISSING_ERROR, - msg="setQuerySettings should reject indexHints.ns missing coll field", - ), - CommandTestCase( - "indexHints_ns_null_coll", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": None}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=QUERYSETTINGS_NS_COLL_MISSING_ERROR, - msg="setQuerySettings should reject indexHints.ns with null coll", - ), - CommandTestCase( - "invalid_query_framework_value", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": "invalidFramework", - }, - }, - error_code=BAD_VALUE_ERROR, - msg="setQuerySettings should reject invalid queryFramework string", - ), - CommandTestCase( - "reject_false_only", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": {"reject": False}, - }, - error_code=QUERYSETTINGS_REJECT_ONLY_ERROR, - msg="setQuerySettings should reject settings with only reject: false", - ), - CommandTestCase( - "missing_settings", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - }, - error_code=MISSING_FIELD_ERROR, - msg="setQuerySettings should reject missing settings field", - ), - CommandTestCase( - "empty_settings", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": {}, - }, - error_code=QUERYSETTINGS_EMPTY_SETTINGS_ERROR, - msg="setQuerySettings should reject empty settings document", - ), - CommandTestCase( - "unrecognized_top_level_field", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - "unknownField": 1, - }, - error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, - msg="setQuerySettings should reject unrecognized top-level field", - ), - CommandTestCase( - "system_collection", - command=lambda ctx: { - "setQuerySettings": { - "find": "system.users", - "filter": {}, - "$db": "admin", - }, - "settings": { - "indexHints": [ - { - "ns": {"db": "admin", "coll": "system.users"}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=QUERYSETTINGS_INTERNAL_DB_ERROR, - msg="setQuerySettings should reject query shapes on internal databases", - ), - CommandTestCase( - "local_database", - command=lambda ctx: { - "setQuerySettings": { - "find": "oplog.rs", - "filter": {}, - "$db": "local", - }, - "settings": { - "indexHints": [ - { - "ns": {"db": "local", "coll": "oplog.rs"}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=QUERYSETTINGS_INTERNAL_DB_ERROR, - msg="setQuerySettings should reject query shapes on local database", - ), - CommandTestCase( - "indexHints_empty_allowed_rejected", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"a4": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": [], - } - ], - }, - }, - error_code=QUERYSETTINGS_REJECT_ONLY_ERROR, - msg="setQuerySettings should reject indexHints with empty allowedIndexes", - ), - CommandTestCase( - "idhack_query_rejected", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"_id": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - error_code=QUERYSETTINGS_IDHACK_QUERY_ERROR, - msg="setQuerySettings should reject IDHACK-eligible queries", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_VALIDATION_ERROR_TESTS)) -def test_setQuerySettings_validation_errors(collection, test): - """Test setQuerySettings structural and validation error rejection.""" - ctx = CommandContext.from_collection(collection) - result = execute_admin_command(collection, test.build_command(ctx)) - assertResult( - result, - error_code=test.error_code, - msg=test.msg, - ) diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py deleted file mode 100644 index 113da8840..000000000 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/test_setQuerySettings_verification.py +++ /dev/null @@ -1,970 +0,0 @@ -"""Tests for setQuerySettings observable effects and verification. - -Validates query shape hash properties, $querySettings stage output for -distinct and aggregate shapes, showDebugQueryShape, multiple settings -management, comment visibility, settings replacement semantics, and -indexHints namespace mismatch acceptance. -""" - -from __future__ import annotations - -import re - -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 -from documentdb_tests.framework.parametrize import pytest_params - -from .utils.setQuerySettings_common import get_query_settings - -# Property [Hash Format]: queryShapeHash is a 64-character hexadecimal string. -# Property [Hash Consistency]: same query shape produces the same hash. -# Property [Hash Uniqueness]: different query shapes produce different hashes. -# Property [Shape Matching]: filter values do not affect shape identity. -# Property [Sort Direction Matters]: different sort directions produce different hashes. -# Property [$querySettings Distinct]: $querySettings returns correct data for distinct. -# Property [$querySettings Aggregate]: $querySettings returns correct data for aggregate. -# Property [showDebugQueryShape True]: debugQueryShape present when requested. -# Property [showDebugQueryShape False]: debugQueryShape absent when not requested. -# Property [Multiple Settings Visible]: all query settings appear in $querySettings. -# Property [Multiple Settings Remove]: removing one leaves others intact. -# Property [Comment Visibility]: settings.comment appears in $querySettings output. -# Property [Comment Update]: updating settings.comment replaces the old value. -# Property [Settings Replacement]: updating settings preserves unmodified fields. -# Property [No Duplicate On Update]: updating same shape does not duplicate entries. -# Property [ns Mismatch]: indexHints ns.coll can differ from query shape collection. - - -# --------------------------------------------------------------------------- -# Group 1: ns.coll mismatch acceptance test -# --------------------------------------------------------------------------- - -SET_QUERY_SETTINGS_NS_MISMATCH_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "ns_coll_mismatch_accepted", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"mis1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": { - "db": ctx.database, - "coll": "completely_different_collection", - }, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"ok": 1.0}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"mis1": 1}, - "$db": ctx.database, - } - } - ], - msg="ns.coll mismatch should be accepted", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_NS_MISMATCH_TESTS)) -def test_setQuerySettings_ns_coll_mismatch_accepted(collection, test): - """Test that indexHints ns.coll can differ from query shape collection.""" - ctx = CommandContext.from_collection(collection) - try: - 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 - - -# --------------------------------------------------------------------------- -# Group 2: Hash property tests -# --------------------------------------------------------------------------- - -SET_QUERY_SETTINGS_HASH_SAME_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "same_shape_produces_same_hash", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"h2": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - ], - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"h2": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"h2": 1}, - "$db": ctx.database, - } - } - ], - msg="same query shape should produce identical hashes", - ), - SettingsTestCase( - "filter_values_do_not_affect_shape", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"x": 1}, - "$db": ctx.database, - } - }, - ], - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"x": 999}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"x": 999}, - "$db": ctx.database, - } - } - ], - msg="filter values should not affect query shape hash", - ), -] - -SET_QUERY_SETTINGS_HASH_DIFFERENT_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "different_shapes_different_hashes", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"h3a": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - ], - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"h3b": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"h3a": 1}, - "$db": ctx.database, - } - }, - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"h3b": 1}, - "$db": ctx.database, - } - }, - ], - msg="different query shapes should produce different hashes", - ), - SettingsTestCase( - "sort_direction_affects_shape", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"sd": 1}, - "sort": {"a": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - ], - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"sd": 1}, - "sort": {"a": -1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"sd": 1}, - "sort": {"a": 1}, - "$db": ctx.database, - } - }, - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"sd": 1}, - "sort": {"a": -1}, - "$db": ctx.database, - } - }, - ], - msg="sort direction should produce different query shape hashes", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_HASH_SAME_TESTS)) -def test_setQuerySettings_hash_same(collection, test): - """Test that equivalent query shapes produce the same hash.""" - ctx = CommandContext.from_collection(collection) - try: - setup_hash = None - for cmd in test.build_setup(ctx): - r = execute_admin_command(collection, cmd) - ctx.setup_results.append(r) - if "queryShapeHash" in r: - setup_hash = r["queryShapeHash"] - result = execute_admin_command(collection, test.build_command(ctx)) - assertSuccessPartial( - result, - {"queryShapeHash": setup_hash}, - msg=test.msg, - ) - finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_HASH_DIFFERENT_TESTS)) -def test_setQuerySettings_hash_different(collection, test): - """Test that distinct query shapes produce different hashes.""" - ctx = CommandContext.from_collection(collection) - try: - setup_hash = None - for cmd in test.build_setup(ctx): - r = execute_admin_command(collection, cmd) - ctx.setup_results.append(r) - if "queryShapeHash" in r: - setup_hash = r["queryShapeHash"] - result = execute_admin_command(collection, test.build_command(ctx)) - hashes_differ = result["queryShapeHash"] != setup_hash - assertSuccessPartial( - {"differ": hashes_differ}, - {"differ": True}, - msg=test.msg, - ) - finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass - - -# --------------------------------------------------------------------------- -# Group 3: Hash format test -# --------------------------------------------------------------------------- - -SET_QUERY_SETTINGS_HASH_FORMAT_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "hash_is_64_char_hex", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"h1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"h1": 1}, - "$db": ctx.database, - } - } - ], - msg="queryShapeHash should be 64-char hex", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_HASH_FORMAT_TESTS)) -def test_setQuerySettings_hash_format(collection, test): - """Test that queryShapeHash matches expected format.""" - ctx = CommandContext.from_collection(collection) - try: - result = execute_admin_command(collection, test.build_command(ctx)) - h = result.get("queryShapeHash", "") - is_valid = bool(re.fullmatch(r"[0-9A-Fa-f]{64}", h)) - assertSuccessPartial( - {"valid": is_valid}, - {"valid": True}, - msg=f"{test.msg}, got: {h!r}", - ) - finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass - - -# --------------------------------------------------------------------------- -# Group 4: $querySettings inspection tests -# --------------------------------------------------------------------------- - -SET_QUERY_SETTINGS_QS_STAGE_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "querySettings_returns_distinct_shape", - command=lambda ctx: { - "setQuerySettings": { - "distinct": ctx.collection, - "key": "x", - "query": {"qs_d1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected=lambda ctx: {"distinct": ctx.collection}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "distinct": ctx.collection, - "key": "x", - "query": {"qs_d1": 1}, - "$db": ctx.database, - } - } - ], - msg="representativeQuery should be a distinct shape", - ), - SettingsTestCase( - "querySettings_returns_aggregate_shape", - command=lambda ctx: { - "setQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"qs_a1": 1}}], - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected=lambda ctx: {"aggregate": ctx.collection}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "aggregate": ctx.collection, - "pipeline": [{"$match": {"qs_a1": 1}}], - "$db": ctx.database, - } - } - ], - msg="representativeQuery should be an aggregate shape", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_QS_STAGE_TESTS)) -def test_setQuerySettings_qs_stage(collection, test): - """Test $querySettings returns correct representativeQuery.""" - ctx = CommandContext.from_collection(collection) - try: - r = execute_admin_command(collection, test.build_command(ctx)) - settings = get_query_settings(collection) - matching = [s for s in settings if s.get("queryShapeHash") == r["queryShapeHash"]] - entry = matching[0] if matching else {} - assertSuccessPartial( - entry.get("representativeQuery", {}), - test.build_expected(ctx), - msg=test.msg, - ) - finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass - - -# --------------------------------------------------------------------------- -# Group 5: showDebugQueryShape tests -# --------------------------------------------------------------------------- - -SET_QUERY_SETTINGS_DEBUG_SHAPE_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "debug_query_shape_present_when_enabled", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"dbg1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"has_debug": True}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"dbg1": 1}, - "$db": ctx.database, - } - } - ], - msg="debugQueryShape should be present with showDebugQueryShape: true", - ), - SettingsTestCase( - "debug_query_shape_absent_when_disabled", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"dbg2": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"has_debug": False}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"dbg2": 1}, - "$db": ctx.database, - } - } - ], - msg="debugQueryShape should be absent with showDebugQueryShape: false", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_DEBUG_SHAPE_TESTS)) -def test_setQuerySettings_debug_shape(collection, test): - """Test showDebugQueryShape controls debugQueryShape presence.""" - ctx = CommandContext.from_collection(collection) - expected = test.build_expected(ctx) - show_debug = expected["has_debug"] - try: - execute_admin_command(collection, test.build_command(ctx)) - settings = list( - collection.database.client.admin.aggregate( - [{"$querySettings": {"showDebugQueryShape": show_debug}}] - ) - ) - filter_key = "dbg1" if show_debug else "dbg2" - entry = [ - s - for s in settings - if s.get("representativeQuery", {}).get("filter", {}).get(filter_key) - ] - has_debug = "debugQueryShape" in (entry[0] if entry else {}) - assertSuccessPartial( - {"has_debug": has_debug}, - expected, - msg=test.msg, - ) - finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass - - -# --------------------------------------------------------------------------- -# Group 6: Settings field verification via $querySettings -# (comment visibility, comment update, settings replacement) -# --------------------------------------------------------------------------- - -SET_QUERY_SETTINGS_FIELD_VERIFICATION_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "comment_visible_in_querySettings", - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"comvis1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "comment": "my-test-comment", - }, - }, - expected={"comment": "my-test-comment"}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"comvis1": 1}, - "$db": ctx.database, - } - } - ], - msg="comment should be visible in $querySettings output", - ), - SettingsTestCase( - "comment_replaced_on_update", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"comup1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "comment": "original", - }, - } - ], - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"comup1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "comment": "updated", - }, - }, - expected={"comment": "updated"}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"comup1": 1}, - "$db": ctx.database, - } - } - ], - msg="comment should be replaced by the updated value", - ), - SettingsTestCase( - "update_preserves_unmodified_fields", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"rep1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": "classic", - }, - } - ], - command=lambda ctx: { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"rep1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - expected={"queryFramework": "classic"}, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"rep1": 1}, - "$db": ctx.database, - } - } - ], - msg="queryFramework should be preserved after update with only indexHints", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_FIELD_VERIFICATION_TESTS)) -def test_setQuerySettings_field_verification(collection, test): - """Test settings fields are visible and correctly updated in $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) - r = execute_admin_command(collection, test.build_command(ctx)) - settings = get_query_settings(collection) - matching = [s for s in settings if s.get("queryShapeHash") == r["queryShapeHash"]] - entry = matching[0] if matching else {} - assertSuccessPartial( - entry.get("settings", {}), - test.build_expected(ctx), - msg=test.msg, - ) - finally: - for cmd in test.build_cleanup(ctx): - try: - execute_admin_command(collection, cmd) - except Exception: - pass - - -# --------------------------------------------------------------------------- -# Group 7: Multi-setup settings management tests -# --------------------------------------------------------------------------- - - -SET_QUERY_SETTINGS_MULTI_SETUP_TESTS: list[SettingsTestCase] = [ - SettingsTestCase( - "no_duplicate_on_update", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"dup1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"dup1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - "queryFramework": "classic", - }, - }, - ], - expected=lambda ctx: { - "ok": sum( - 1 - for h in ctx.setup_results[-1]["_live_hashes"] - if h - == [r["queryShapeHash"] for r in ctx.setup_results if "queryShapeHash" in r][-1] - ) - == 1 - }, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"dup1": 1}, - "$db": ctx.database, - } - } - ], - msg="updating same shape should not create duplicate entries", - ), - SettingsTestCase( - "multiple_settings_all_visible", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {f"multi{i}": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - } - for i in range(1, 4) - ], - expected=lambda ctx: { - "ok": all( - h in ctx.setup_results[-1]["_live_hashes"] - for h in [r["queryShapeHash"] for r in ctx.setup_results if "queryShapeHash" in r] - ) - }, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {f"multi{i}": 1}, - "$db": ctx.database, - } - } - for i in range(1, 4) - ], - msg="all 3 query settings should be visible in $querySettings", - ), - SettingsTestCase( - "remove_one_leaves_others", - setup_commands=lambda ctx: [ - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"rem1": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - { - "setQuerySettings": { - "find": ctx.collection, - "filter": {"rem2": 1}, - "$db": ctx.database, - }, - "settings": { - "indexHints": [ - { - "ns": {"db": ctx.database, "coll": ctx.collection}, - "allowedIndexes": ["_id_"], - } - ], - }, - }, - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {"rem1": 1}, - "$db": ctx.database, - } - }, - ], - expected=lambda ctx: { - "ok": [r["queryShapeHash"] for r in ctx.setup_results if "queryShapeHash" in r][0] - not in ctx.setup_results[-1]["_live_hashes"] - and [r["queryShapeHash"] for r in ctx.setup_results if "queryShapeHash" in r][1] - in ctx.setup_results[-1]["_live_hashes"] - }, - cleanup=lambda ctx: [ - { - "removeQuerySettings": { - "find": ctx.collection, - "filter": {f"rem{i}": 1}, - "$db": ctx.database, - } - } - for i in range(1, 3) - ], - msg="q1 removed, q2 should remain in $querySettings", - ), -] - - -@pytest.mark.admin -@pytest.mark.replica_set -@pytest.mark.parametrize("test", pytest_params(SET_QUERY_SETTINGS_MULTI_SETUP_TESTS)) -def test_setQuerySettings_multi_setup(collection, test): - """Test multi-setup settings management via $querySettings inspection.""" - ctx = CommandContext.from_collection(collection) - try: - for cmd in test.build_setup(ctx): - r = execute_admin_command(collection, cmd) - ctx.setup_results.append(r) - all_hashes = {s.get("queryShapeHash") for s in get_query_settings(collection)} - # Stash live hashes so expected-lambdas can reference them. - ctx.setup_results.append({"_live_hashes": all_hashes}) - assertSuccessPartial( - test.build_expected(ctx), - {"ok": True}, - 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/setQuerySettings/utils/__init__.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/setQuerySettings_common.py b/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/setQuerySettings_common.py deleted file mode 100644 index 9d5da0037..000000000 --- a/documentdb_tests/compatibility/tests/core/query_planning/commands/setQuerySettings/utils/setQuerySettings_common.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Shared utilities for setQuerySettings tests.""" - -from __future__ import annotations - -from typing import Any - -from pymongo.collection import Collection - - -def get_query_settings(collection: Collection) -> list[dict[str, Any]]: - """Retrieve all current query settings via $querySettings stage.""" - admin = collection.database.client.admin - result = admin.command({"aggregate": 1, "pipeline": [{"$querySettings": {}}], "cursor": {}}) - batch: list[dict[str, Any]] = result.get("cursor", {}).get("firstBatch", []) - return batch From 356dbde08fae0db215b39d4616c3471808446597 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 16 Jun 2026 13:42:56 -0700 Subject: [PATCH 27/27] remove unused error codes Signed-off-by: Alina (Xi) Li --- documentdb_tests/framework/error_codes.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/documentdb_tests/framework/error_codes.py b/documentdb_tests/framework/error_codes.py index 7c137d582..cf6d4e8ad 100644 --- a/documentdb_tests/framework/error_codes.py +++ b/documentdb_tests/framework/error_codes.py @@ -53,7 +53,6 @@ API_VERSION_ERROR = 322 API_STRICT_ERROR = 323 COLLECTION_UUID_MISMATCH_ERROR = 361 -QUERYSETTINGS_QUERY_REJECTED_ERROR = 411 EXPRESSION_NOT_OBJECT_ERROR = 10065 BSON_OBJECT_TOO_LARGE_ERROR = 10334 DUPLICATE_KEY_ERROR = 11000 @@ -502,17 +501,11 @@ GEO_NEAR_MIN_DISTANCE_NOT_CONSTANT_ERROR = 7555701 GEO_NEAR_MAX_DISTANCE_NOT_CONSTANT_ERROR = 7555702 QUERYSETTINGS_UNKNOWN_COMMAND_SHAPE_ERROR = 7746402 -QUERYSETTINGS_REJECT_ONLY_ERROR = 7746604 -QUERYSETTINGS_IDHACK_QUERY_ERROR = 7746606 QUERYSETTINGS_NON_DOCUMENT_ARG_ERROR = 7746800 PIPELINE_LENGTH_LIMIT_ERROR = 7749501 PERCENTILE_INVALID_P_FIELD_ERROR = 7750301 PERCENTILE_INVALID_P_VALUE_ERROR = 7750303 ENCRYPTED_FIELD_TRIM_FACTOR_OUT_OF_RANGE_ERROR = 8574000 -QUERYSETTINGS_INTERNAL_DB_ERROR = 8584900 -QUERYSETTINGS_NS_DB_MISSING_ERROR = 8727500 -QUERYSETTINGS_NS_COLL_MISSING_ERROR = 8727501 -QUERYSETTINGS_EMPTY_SETTINGS_ERROR = 8727502 COUNT_FIELD_ID_RESERVED_ERROR = 9039800 CONVERT_BYTE_ORDER_TYPE_ERROR = 9130001 CONVERT_BYTE_ORDER_VALUE_ERROR = 9130002