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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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 da9de75694de950b8b40444084dab2607caa3c5a Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 16 Jun 2026 11:44:09 -0700 Subject: [PATCH 18/19] 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 cd8c4e7e9e7a9ee36edbfad43cdb0fad2f1a4f48 Mon Sep 17 00:00:00 2001 From: "Alina (Xi) Li" Date: Tue, 16 Jun 2026 12:06:03 -0700 Subject: [PATCH 19/19] 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