diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/__init__.py b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_bson_validation.py b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_bson_validation.py new file mode 100644 index 000000000..32f7d5567 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_bson_validation.py @@ -0,0 +1,92 @@ +"""Tests for wildcard index BSON type validation.""" + +import pytest +from bson import Int64 +from bson.decimal128 import Decimal128 + +from documentdb_tests.framework.assertions import assertFailureCode, assertNotError +from documentdb_tests.framework.bson_type_validator import ( + BsonType, + BsonTypeTestCase, + generate_bson_acceptance_test_cases, + generate_bson_rejection_test_cases, +) +from documentdb_tests.framework.error_codes import ( + CANNOT_CREATE_INDEX_ERROR, + TYPE_MISMATCH_ERROR, +) +from documentdb_tests.framework.executor import execute_command + +pytestmark = pytest.mark.index + + +WILDCARD_BSON_PARAMS = [ + BsonTypeTestCase( + id="wildcardProjection", + msg="wildcardProjection should reject non-document types", + keyword="wildcardProjection", + valid_types=[BsonType.OBJECT], + default_error_code=TYPE_MISMATCH_ERROR, + valid_inputs={BsonType.OBJECT: {"a": 1}}, + ), + BsonTypeTestCase( + id="wildcard_key_value", + msg="wildcard index key value should reject non-numeric types", + keyword="key_value", + valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL], + default_error_code=CANNOT_CREATE_INDEX_ERROR, + # STRING is skipped here because some strings name valid special index + # types (e.g. "2d", "2dsphere", "hashed", "wildcard"); string key values + # are covered explicitly in test_wildcard_errors.py. + skip_rejection_types=[BsonType.STRING], + valid_inputs={ + BsonType.DOUBLE: 1.0, + BsonType.INT: 1, + BsonType.LONG: Int64(1), + BsonType.DECIMAL: Decimal128("1"), + }, + ), +] + +REJECTION_CASES = generate_bson_rejection_test_cases(WILDCARD_BSON_PARAMS) +ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(WILDCARD_BSON_PARAMS) + + +def _build_command(collection, spec, sample_value): + """Build the appropriate createIndexes command for the given spec and sample value.""" + if spec.id == "wildcardProjection": + return execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [ + { + "key": {"$**": 1}, + "name": "wc_bson_test", + "wildcardProjection": sample_value, + } + ], + }, + ) + else: + return execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"$**": sample_value}, "name": "wc_bson_test"}], + }, + ) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", REJECTION_CASES) +def test_wildcard_rejects_invalid_bson_type(collection, bson_type, sample_value, spec): + """Test wildcard index options reject invalid BSON types.""" + result = _build_command(collection, spec, sample_value) + assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) +def test_wildcard_accepts_valid_bson_type(collection, bson_type, sample_value, spec): + """Test wildcard index options accept valid BSON types.""" + result = _build_command(collection, spec, sample_value) + assertNotError(result, msg=f"{spec.id} should accept {bson_type.value}") diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_creation.py b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_creation.py new file mode 100644 index 000000000..c97b26bd7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_creation.py @@ -0,0 +1,478 @@ +"""Tests for wildcard index creation, management, and projection.""" + +import pytest + +from documentdb_tests.compatibility.tests.core.indexes.commands.utils.index_test_case import ( + IndexQueryTestCase, + IndexTestCase, +) +from documentdb_tests.framework.assertions import ( + assertFailureCode, + assertSuccess, + assertSuccessPartial, +) +from documentdb_tests.framework.error_codes import INDEX_KEY_SPECS_CONFLICT_ERROR +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.index + + +VALID_PROJECTION_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="include_single_field", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"a": 1}},), + msg="Inclusion projection of one field should succeed", + ), + IndexTestCase( + id="exclude_single_field", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"a": 0}},), + msg="Exclusion projection of one field should succeed", + ), + IndexTestCase( + id="include_multiple_fields", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"a": 1, "b": 1}},), + msg="Inclusion projection of multiple fields should succeed", + ), + IndexTestCase( + id="exclude_multiple_fields", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"a": 0, "b": 0}},), + msg="Exclusion projection of multiple fields should succeed", + ), + IndexTestCase( + id="exclude_id_in_inclusion", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"_id": 0, "a": 1}},), + msg="Excluding _id in an inclusion projection should succeed (_id exception)", + ), + IndexTestCase( + id="include_id_in_exclusion", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"_id": 1, "a": 0}},), + msg="Including _id in an exclusion projection should succeed (_id exception)", + ), + IndexTestCase( + id="include_dotted_path", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"a.b": 1}},), + msg="Inclusion projection of a dotted path should succeed", + ), + IndexTestCase( + id="include_nested_object_form", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"a": {"b": 1}}},), + msg="Inclusion projection in nested-object form should succeed", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(VALID_PROJECTION_TESTS)) +def test_wildcard_projection_valid(collection, test): + """Verify valid wildcardProjection configurations are accepted.""" + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + assertSuccessPartial(result, {"numIndexesAfter": 2, "ok": 1.0}, msg=test.msg) + + +VALID_CREATION_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="all_fields_ascending", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + msg="All-fields ascending wildcard index should be created", + ), + IndexTestCase( + id="all_fields_descending", + indexes=({"key": {"$**": -1}, "name": "wc_all_desc"},), + msg="All-fields descending wildcard index should be created", + ), + IndexTestCase( + id="single_field_ascending", + indexes=({"key": {"sub.$**": 1}, "name": "wc_sub"},), + msg="Single-field ascending wildcard index should be created", + ), + IndexTestCase( + id="single_field_descending", + indexes=({"key": {"sub.$**": -1}, "name": "wc_sub_desc"},), + msg="Single-field descending wildcard index should be created", + ), + IndexTestCase( + id="deeply_nested_path", + indexes=({"key": {"a.b.c.$**": 1}, "name": "wc_deep"},), + msg="Deeply nested scoped wildcard index should be created", + ), + IndexTestCase( + id="background_option", + indexes=({"key": {"$**": 1}, "name": "wc_bg", "background": True},), + msg="Wildcard index with background:true (legacy no-op) should be created", + ), + IndexTestCase( + id="partial_filter", + indexes=( + { + "key": {"$**": 1}, + "name": "wc_partial", + "partialFilterExpression": {"a": {"$gt": 0}}, + }, + ), + msg="Wildcard index with partialFilterExpression should be created", + ), + IndexTestCase( + id="collation", + indexes=({"key": {"$**": 1}, "name": "wc_collation", "collation": {"locale": "en"}},), + msg="Wildcard index with collation should be created", + ), + IndexTestCase( + id="rooted_at_id", + indexes=({"key": {"_id.$**": 1}, "name": "wc_id_sub"},), + msg="Scoped wildcard index rooted at _id should be created", + ), + IndexTestCase( + id="key_infinity", + indexes=({"key": {"$**": float("inf")}, "name": "wc_inf"},), + msg="Wildcard key with Infinity direction should be created (any nonzero number is valid)", + ), + IndexTestCase( + id="key_negative_infinity", + indexes=({"key": {"$**": float("-inf")}, "name": "wc_neg_inf"},), + msg="Wildcard key with -Infinity direction should be created (any nonzero number is valid)", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(VALID_CREATION_TESTS)) +def test_wildcard_create_valid_spec(collection, test): + """Verify valid wildcard index key patterns and options are accepted.""" + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + assertSuccessPartial( + result, + {"numIndexesBefore": 1, "numIndexesAfter": 2, "ok": 1.0}, + msg=test.msg, + ) + + +COMPOUND_WILDCARD_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="nonwildcard_first", + indexes=({"key": {"a": 1, "$**": 1}, "name": "cwi_a_wc", "wildcardProjection": {"a": 0}},), + msg="Compound wildcard (non-wildcard first) should be created", + ), + IndexTestCase( + id="wildcard_first", + indexes=({"key": {"$**": 1, "a": 1}, "name": "cwi_wc_a", "wildcardProjection": {"a": 0}},), + msg="Compound wildcard (wildcard first) should be created", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(COMPOUND_WILDCARD_TESTS)) +def test_compound_wildcard_created(collection, test): + """Verify compound wildcard indexes are created with the wildcard term in either position.""" + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + assertSuccessPartial(result, {"numIndexesAfter": 2, "ok": 1.0}, msg=test.msg) + + +DROP_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="by_name", + input="wc_all", + msg="Drop wildcard index by name", + ), + IndexTestCase( + id="by_key_pattern", + input={"$**": 1}, + msg="Drop wildcard index by key pattern", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(DROP_TESTS)) +def test_wildcard_drop(collection, test): + """Verify dropIndexes removes the wildcard index by name or key pattern.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + result = execute_command(collection, {"dropIndexes": collection.name, "index": test.input}) + assertSuccessPartial(result, {"nIndexesWas": 2, "ok": 1.0}, msg=test.msg) + + +COEXIST_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="two_wildcard_indexes", + indexes=( + {"key": {"$**": 1}, "name": "wc_all"}, + {"key": {"sub.$**": 1}, "name": "wc_sub"}, + ), + msg="Two wildcard indexes with different key patterns should coexist", + ), + IndexTestCase( + id="wildcard_with_targeted_index", + indexes=( + {"key": {"$**": 1}, "name": "wc_all"}, + {"key": {"a": 1}, "name": "a_1"}, + ), + msg="Wildcard index coexists with targeted index on an overlapping field", + ), + IndexTestCase( + id="two_compound_wildcards_same_prefix_different_scope", + indexes=( + {"key": {"a": 1, "$**": 1}, "name": "cwi_ab", "wildcardProjection": {"a": 0, "b": 0}}, + {"key": {"a": 1, "$**": 1}, "name": "cwi_ac", "wildcardProjection": {"a": 0, "c": 0}}, + ), + msg="Two compound wildcard indexes with the same prefix but different wildcard scopes " + "(differing projections) and distinct names should coexist", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(COEXIST_TESTS)) +def test_wildcard_coexists(collection, test): + """Verify a wildcard index can coexist with other indexes on the same collection.""" + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + assertSuccessPartial( + result, + {"numIndexesBefore": 1, "numIndexesAfter": 3, "ok": 1.0}, + msg=test.msg, + ) + + +PROJECTION_QUERY_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="inclusion_included_field", + indexes=({"key": {"$**": 1}, "name": "wc_inc", "wildcardProjection": {"a": 1}},), + doc=({"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 3, "b": 4}), + filter={"a": 1}, + hint="wc_inc", + expected=[{"_id": 1, "a": 1, "b": 2}], + msg="Query on included field via wildcard", + ), + IndexQueryTestCase( + id="exclusion_non_excluded_field", + indexes=({"key": {"$**": 1}, "name": "wc_exc", "wildcardProjection": {"a": 0}},), + doc=({"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 3, "b": 4}), + filter={"b": 2}, + hint="wc_exc", + expected=[{"_id": 1, "a": 1, "b": 2}], + msg="Query on non-excluded field via wildcard", + ), + IndexQueryTestCase( + id="excluded_field_unhinted", + indexes=({"key": {"$**": 1}, "name": "wc_exc", "wildcardProjection": {"a": 0}},), + doc=({"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 5, "b": 4}), + filter={"a": 5}, + expected=[{"_id": 2, "a": 5, "b": 4}], + msg="Excluded-field query correctness (via collection scan)", + ), + IndexQueryTestCase( + id="dotted_path_projection_served", + indexes=({"key": {"$**": 1}, "name": "wc_dot", "wildcardProjection": {"a.b": 1}},), + doc=({"_id": 1, "a": {"b": 1}}, {"_id": 2, "a": {"b": 2}}), + filter={"a.b": 2}, + hint="wc_dot", + expected=[{"_id": 2, "a": {"b": 2}}], + msg="Query on a dotted-path projected subtree is served by the wildcard index", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(PROJECTION_QUERY_TESTS)) +def test_wildcard_projection_query(collection, test): + """Verify query correctness on fields included in, excluded from, or outside a + wildcardProjection.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter} + if test.hint: + cmd["hint"] = test.hint + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +def test_wildcard_auto_generated_name(collection): + """Creating a wildcard index without specifying a name yields the conventional + auto-generated '$**_1' name. The name is generated client-side by the driver helper + (the raw createIndexes command requires an explicit name).""" + collection.create_index([("$**", 1)]) + result = execute_command(collection, {"listIndexes": collection.name}) + assertSuccess( + result, + ["$**_1", "_id_"], + transform=lambda batch: sorted(idx["name"] for idx in batch), + msg="Wildcard index without an explicit name should be auto-named '$**_1'", + ) + + +def test_wildcard_create_on_empty_collection(collection): + """Creating a wildcard index on an existing empty collection succeeds.""" + execute_command(collection, {"insert": collection.name, "documents": [{"_id": 1}]}) + execute_command(collection, {"delete": collection.name, "deletes": [{"q": {}, "limit": 0}]}) + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_empty"}]}, + ) + assertSuccessPartial( + result, + {"numIndexesAfter": 2, "ok": 1.0}, + msg="Wildcard index on empty collection should be created", + ) + + +def test_wildcard_create_idempotent(collection): + """Re-creating an identical wildcard index is a no-op (same index count).""" + spec = {"key": {"$**": 1}, "name": "wc_idem"} + execute_command(collection, {"createIndexes": collection.name, "indexes": [spec]}) + result = execute_command(collection, {"createIndexes": collection.name, "indexes": [spec]}) + assertSuccessPartial( + result, + {"numIndexesBefore": 2, "numIndexesAfter": 2, "ok": 1.0}, + msg="Re-creating identical wildcard index should be a no-op", + ) + + +def test_wildcard_same_name_different_projection_conflicts(collection): + """Re-creating a wildcard index with the same name but a different wildcardProjection + fails with IndexKeySpecsConflict.""" + execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"$**": 1}, "name": "wc_conf", "wildcardProjection": {"a": 1}}], + }, + ) + result = execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"$**": 1}, "name": "wc_conf", "wildcardProjection": {"b": 1}}], + }, + ) + assertFailureCode( + result, + INDEX_KEY_SPECS_CONFLICT_ERROR, + msg="Same name with a different wildcardProjection should fail with IndexKeySpecsConflict", + ) + + +def test_wildcard_index_on_clustered_collection(collection): + """A wildcard index can be created on a clustered collection.""" + name = f"{collection.name}_clustered" + execute_command( + collection, + {"create": name, "clusteredIndex": {"key": {"_id": 1}, "unique": True}}, + ) + result = execute_command( + collection, + {"createIndexes": name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + assertSuccessPartial( + result, + {"numIndexesBefore": 0, "numIndexesAfter": 1, "ok": 1.0}, + msg="Wildcard index on clustered collection should succeed", + ) + + +def test_wildcard_and_text_coexist_non_text_query(collection): + """With both wildcard and text indexes, a non-$text query still uses the wildcard index.""" + execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [ + {"key": {"$**": 1}, "name": "wc_all"}, + {"key": {"t": "text"}, "name": "t_text"}, + ], + }, + ) + collection.insert_many([{"_id": 1, "t": "hello", "n": 1}, {"_id": 2, "t": "world", "n": 2}]) + result = execute_command( + collection, {"find": collection.name, "filter": {"n": 2}, "hint": "wc_all"} + ) + assertSuccess( + result, [{"_id": 2, "t": "world", "n": 2}], msg="Non-$text query uses wildcard index" + ) + + +def test_wildcard_drop_scoped_by_key_pattern(collection): + """dropIndexes removes a scoped (subtree) wildcard index specified by its key pattern.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"sub.$**": 1}, "name": "wc_sub"}]}, + ) + result = execute_command(collection, {"dropIndexes": collection.name, "index": {"sub.$**": 1}}) + assertSuccessPartial( + result, + {"nIndexesWas": 2, "ok": 1.0}, + msg="Drop scoped wildcard index by key pattern", + ) + + +def test_wildcard_query_after_drop_correct(collection): + """After dropping the wildcard index, queries on previously indexed fields still return + results.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + collection.insert_many([{"_id": 1, "a": 1}, {"_id": 2, "a": 2}]) + execute_command(collection, {"dropIndexes": collection.name, "index": "wc_all"}) + result = execute_command(collection, {"find": collection.name, "filter": {"a": 2}}) + assertSuccess(result, [{"_id": 2, "a": 2}], msg="Query correctness after wildcard index drop") + + +def test_wildcard_listindexes_reflects_index(collection): + """listIndexes reflects the wildcard index with its key and name.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + result = execute_command(collection, {"listIndexes": collection.name}) + + def _wc_entry(batch): + for idx in batch: + if idx["name"] == "wc_all": + return {"key": idx["key"], "name": idx["name"]} + return None + + assertSuccess( + result, + {"key": {"$**": 1}, "name": "wc_all"}, + transform=_wc_entry, + msg="listIndexes reflects wildcard index key and name", + ) + + +def test_wildcard_projection_persisted_in_listindexes(collection): + """A wildcardProjection is persisted and reported by listIndexes.""" + execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [ + {"key": {"$**": 1}, "name": "wc_proj", "wildcardProjection": {"a": 0, "b": 0}} + ], + }, + ) + result = execute_command(collection, {"listIndexes": collection.name}) + + def _get_proj(batch): + for idx in batch: + if idx["name"] == "wc_proj": + return idx.get("wildcardProjection") + return None + + assertSuccess( + result, + {"a": 0, "b": 0}, + transform=_get_proj, + msg="wildcardProjection should be persisted in listIndexes", + ) diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_data_types.py b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_data_types.py new file mode 100644 index 000000000..c9672659f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_data_types.py @@ -0,0 +1,709 @@ +"""Tests for wildcard index data type handling: document structure, BSON scalar and special types, +objects, arrays, null/missing semantics, and write operations.""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Timestamp + +from documentdb_tests.compatibility.tests.core.indexes.commands.utils.index_test_case import ( + IndexQueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess, assertSuccessPartial +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.index + + +STRUCTURE_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="embedded_doc_equality_unhinted", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": {"b": 1}}, {"_id": 2, "a": {"b": 2}}), + # Whole-document equality is not served by the wildcard index; run unhinted. + filter={"a": {"b": 1}}, + expected=[{"_id": 1, "a": {"b": 1}}], + msg="Whole-document equality correctness", + ), + IndexQueryTestCase( + id="array_equality_unhinted", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": [1, 2]}, {"_id": 2, "a": [1, 2, 3]}), + # Whole-array equality is not served by the wildcard index; run unhinted. + filter={"a": [1, 2]}, + expected=[{"_id": 1, "a": [1, 2]}], + msg="Whole-array equality correctness", + ), + IndexQueryTestCase( + id="empty_array_equality", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": []}, {"_id": 2, "a": [1]}, {"_id": 3, "a": 5}), + filter={"a": []}, + hint="wc_all", + expected=[{"_id": 1, "a": []}], + msg="Empty array equality via wildcard", + ), + IndexQueryTestCase( + id="empty_array_nested_in_object", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": {"b": []}}, {"_id": 2, "a": {"b": [1]}}), + filter={"a.b": []}, + hint="wc_all", + expected=[{"_id": 1, "a": {"b": []}}], + msg="Empty array nested in object", + ), + IndexQueryTestCase( + id="empty_document_equality_hinted", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": {}}, {"_id": 2, "a": {"b": 1}}, {"_id": 3, "a": 5}), + filter={"a": {}}, + hint="wc_all", + expected=[{"_id": 1, "a": {}}], + msg="Empty-document equality served by wildcard (documented exception)", + ), + IndexQueryTestCase( + id="dedup_exists_on_embedded_object", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": {"x": 1, "y": 2, "z": 3}},), + filter={"a": {"$exists": True}}, + hint="wc_all", + expected=[{"_id": 1, "a": {"x": 1, "y": 2, "z": 3}}], + msg="$exists on multi-leaf embedded object returns doc once (dedup)", + ), + IndexQueryTestCase( + id="dedup_range_on_array_of_subdocs", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "items": [{"q": 5}, {"q": 10}, {"q": 15}]}, + {"_id": 2, "items": [{"q": 1}]}, + ), + filter={"items.q": {"$gte": 5}}, + hint="wc_all", + sort={"_id": 1}, + expected=[{"_id": 1, "items": [{"q": 5}, {"q": 10}, {"q": 15}]}], + msg="Range on array-of-subdocuments returns each matching doc once (multikey dedup)", + ), + IndexQueryTestCase( + id="dedup_exists_on_array_of_subdocs", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "items": [{"q": 5}, {"q": 10}, {"q": 15}]}, + {"_id": 2, "items": [{"q": 1}]}, + ), + filter={"items.q": {"$exists": True}}, + hint="wc_all", + sort={"_id": 1}, + expected=[ + {"_id": 1, "items": [{"q": 5}, {"q": 10}, {"q": 15}]}, + {"_id": 2, "items": [{"q": 1}]}, + ], + msg="$exists on array-of-subdocuments returns each matching doc once (multikey dedup)", + ), + IndexQueryTestCase( + id="numeric_string_field_name", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "obj": {"0": "zero"}}, {"_id": 2, "obj": {"0": "other"}}), + filter={"obj.0": "zero"}, + hint="wc_all", + expected=[{"_id": 1, "obj": {"0": "zero"}}], + msg="Numeric-string field name is queryable", + ), + IndexQueryTestCase( + id="polymorphic_scalar_match", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "x": 5}, {"_id": 2, "x": {"y": 1}}, {"_id": 3, "x": [7, 8]}), + filter={"x": 5}, + hint="wc_all", + expected=[{"_id": 1, "x": 5}], + msg="Scalar match on polymorphic field", + ), + IndexQueryTestCase( + id="polymorphic_array_element_match", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "x": 5}, {"_id": 2, "x": {"y": 1}}, {"_id": 3, "x": [7, 8]}), + filter={"x": 7}, + hint="wc_all", + expected=[{"_id": 3, "x": [7, 8]}], + msg="Array-element match on polymorphic field", + ), + IndexQueryTestCase( + id="polymorphic_nested_match", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "x": 5}, {"_id": 2, "x": {"y": 1}}, {"_id": 3, "x": [7, 8]}), + filter={"x.y": 1}, + hint="wc_all", + expected=[{"_id": 2, "x": {"y": 1}}], + msg="Nested match on polymorphic field", + ), + IndexQueryTestCase( + id="explicit_array_index_element", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": [10, 20]}, {"_id": 2, "a": [30, 40]}), + filter={"a.0": 10}, + sort={"_id": 1}, + expected=[{"_id": 1, "a": [10, 20]}], + msg="Explicit array index element match", + ), + IndexQueryTestCase( + id="explicit_array_index_into_object", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": [{"b": 1}, {"b": 2}]}, {"_id": 2, "a": [{"b": 3}]}), + filter={"a.0.b": 1}, + sort={"_id": 1}, + expected=[{"_id": 1, "a": [{"b": 1}, {"b": 2}]}], + msg="Explicit array index into object", + ), + IndexQueryTestCase( + id="numeric_prefix_path", + indexes=({"key": {"a.0.$**": 1}, "name": "wc_a0"},), + doc=({"_id": 1, "a": [{"b": 1}]}, {"_id": 2, "a": [{"b": 2}]}), + filter={"a.0.b": 2}, + sort={"_id": 1}, + expected=[{"_id": 2, "a": [{"b": 2}]}], + msg="Numeric prefix path query correctness (scoped wildcard on a.0)", + ), + IndexQueryTestCase( + id="leading_zero_index_path", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": [10, 20]}, {"_id": 2, "a": [30]}), + filter={"a.01": 10}, + expected=[], + msg="Leading-zero index path matches no array positions", + ), + IndexQueryTestCase( + id="object_inequality", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": {"b": 1}}, {"_id": 2, "a": {"b": 2}}), + filter={"a": {"$ne": {"b": 1}}}, + sort={"_id": 1}, + expected=[{"_id": 2, "a": {"b": 2}}], + msg="Document inequality correctness", + ), + IndexQueryTestCase( + id="in_with_object_member", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": {"b": 1}}, {"_id": 2, "a": 2}, {"_id": 3, "a": 3}), + filter={"a": {"$in": [{"b": 1}, 2]}}, + sort={"_id": 1}, + expected=[{"_id": 1, "a": {"b": 1}}, {"_id": 2, "a": 2}], + msg="$in with object member correctness", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(STRUCTURE_TESTS)) +def test_wildcard_document_structure(collection, test): + """Verify wildcard index queries on embedded documents, arrays, and mixed-type fields.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter} + if test.hint: + cmd["hint"] = test.hint + if test.sort: + cmd["sort"] = test.sort + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +NULL_MISSING_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="null_matches_null_and_missing", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": None}, {"_id": 2}, {"_id": 3, "a": 5}), + filter={"a": None}, + sort={"_id": 1}, + expected=[{"_id": 1, "a": None}, {"_id": 2}], + msg="{a: null} matches null value and missing field", + ), + IndexQueryTestCase( + id="exists_false_returns_missing", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": None}, {"_id": 2}, {"_id": 3, "a": 5}), + filter={"a": {"$exists": False}}, + sort={"_id": 1}, + expected=[{"_id": 2}], + msg="$exists:false returns docs missing the field", + ), + IndexQueryTestCase( + id="exists_true_hinted", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": None}, {"_id": 2}, {"_id": 3, "a": 5}), + filter={"a": {"$exists": True}}, + hint="wc_all", + sort={"_id": 1}, + expected=[{"_id": 1, "a": None}, {"_id": 3, "a": 5}], + msg="$exists:true via wildcard returns docs where field exists (incl. null)", + ), + IndexQueryTestCase( + id="ne_null_hinted", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": None}, {"_id": 2}, {"_id": 3, "a": 5}), + filter={"a": {"$ne": None}}, + hint="wc_all", + sort={"_id": 1}, + expected=[{"_id": 3, "a": 5}], + msg="$ne null returns non-null existing values", + ), + IndexQueryTestCase( + id="ne_null_on_array_field", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": [1, 2]}, {"_id": 2}, {"_id": 3, "a": 5}), + filter={"a": {"$ne": None}}, + sort={"_id": 1}, + expected=[{"_id": 1, "a": [1, 2]}, {"_id": 3, "a": 5}], + msg="$ne null on array field", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(NULL_MISSING_TESTS)) +def test_wildcard_null_and_missing(collection, test): + """Verify null and missing-field semantics with a wildcard index.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter, "sort": test.sort} + if test.hint: + cmd["hint"] = test.hint + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +def test_wildcard_missing_field_not_indexed_count(collection): + """A document missing the indexed field is not indexed; $exists:true count excludes it.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + collection.insert_many( + [{"_id": 1, "a": 1}, {"_id": 2, "a": None}, {"_id": 3}, {"_id": 4, "a": 3}] + ) + result = execute_command( + collection, + {"count": collection.name, "query": {"a": {"$exists": True}}, "hint": "wc_all"}, + ) + assertSuccessPartial(result, {"n": 3, "ok": 1.0}, msg="Missing field not indexed") + + +def test_wildcard_update_hinted(collection): + """An update hinted to the wildcard index modifies the targeted document.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + collection.insert_many([{"_id": 1, "a": 1}, {"_id": 2, "a": 2}]) + result = execute_command( + collection, + { + "update": collection.name, + "updates": [{"q": {"a": 2}, "u": {"$set": {"a": 99}}, "hint": "wc_all"}], + }, + ) + assertSuccessPartial( + result, {"n": 1, "nModified": 1, "ok": 1.0}, msg="Hinted update via wildcard" + ) + + +def test_wildcard_findandmodify_hinted(collection): + """A findAndModify hinted to the wildcard index updates and returns the modified document.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + collection.insert_many([{"_id": 1, "a": 1}, {"_id": 2, "a": 2}]) + result = execute_command( + collection, + { + "findAndModify": collection.name, + "query": {"a": 2}, + "update": {"$set": {"a": 99}}, + "new": True, + "hint": "wc_all", + }, + ) + assertSuccessPartial( + result, {"value": {"_id": 2, "a": 99}, "ok": 1.0}, msg="Hinted findAndModify via wildcard" + ) + + +def test_wildcard_delete_hinted(collection): + """A delete hinted to the wildcard index removes the targeted document.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + collection.insert_many([{"_id": 1, "a": 1}, {"_id": 2, "a": 2}]) + result = execute_command( + collection, + { + "delete": collection.name, + "deletes": [{"q": {"a": 2}, "limit": 1, "hint": "wc_all"}], + }, + ) + assertSuccessPartial(result, {"n": 1, "ok": 1.0}, msg="Hinted delete via wildcard") + + +def test_wildcard_dynamic_field_indexed_after_update(collection): + """A field added by $set becomes immediately queryable via the wildcard index.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + collection.insert_one({"_id": 1}) + execute_command( + collection, + {"update": collection.name, "updates": [{"q": {"_id": 1}, "u": {"$set": {"newf": 7}}}]}, + ) + result = execute_command( + collection, {"find": collection.name, "filter": {"newf": 7}, "hint": "wc_all"} + ) + assertSuccess( + result, [{"_id": 1, "newf": 7}], msg="Newly added field is queryable via wildcard index" + ) + + +def test_wildcard_overwritten_nested_field_reindexed_after_update(collection): + """Overwriting an existing nested field via $set re-indexes it under the new value: the new + value is queryable via the wildcard index while the old value no longer matches.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + collection.insert_one({"_id": 1, "a": {"b": 1}}) + execute_command( + collection, + {"update": collection.name, "updates": [{"q": {"_id": 1}, "u": {"$set": {"a.b": 99}}}]}, + ) + # A single hinted query on the range [1, 99] proves both facts at once: only the new value + # (99) matches and the old value (1) does not. + result = execute_command( + collection, + {"find": collection.name, "filter": {"a.b": {"$in": [1, 99]}}, "hint": "wc_all"}, + ) + assertSuccess( + result, + [{"_id": 1, "a": {"b": 99}}], + msg="Overwritten nested field is re-indexed: new value matches, old value does not", + ) + + +def test_wildcard_upsert_indexed(collection): + """A document inserted via upsert is queryable via the wildcard index.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + execute_command( + collection, + { + "update": collection.name, + "updates": [{"q": {"a": 42}, "u": {"$set": {"a": 42}}, "upsert": True}], + }, + ) + result = execute_command( + collection, {"find": collection.name, "filter": {"a": 42}, "hint": "wc_all"} + ) + assertSuccess( + result, + [{"a": 42}], + transform=lambda batch: [{"a": d["a"]} for d in batch], + msg="Upserted document is queryable via wildcard index", + ) + + +def test_wildcard_type_undefined_not_null(collection): + """$type 'undefined' does not match a null-valued field.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + collection.insert_many([{"_id": 1, "v": None}, {"_id": 2, "v": 5}]) + result = execute_command( + collection, + {"find": collection.name, "filter": {"v": {"$type": "undefined"}}, "hint": "wc_all"}, + ) + assertSuccess(result, [], msg="$type 'undefined' does not match null") + + +def test_wildcard_type_array_matches_nested_array(collection): + """$type 'array' matches a field whose element is itself an array (nested array).""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + collection.insert_many([{"_id": 1, "v": [[1, 2]]}, {"_id": 2, "v": 5}]) + result = execute_command( + collection, + {"find": collection.name, "filter": {"v": {"$type": "array"}}, "hint": "wc_all"}, + ) + assertSuccess(result, [{"_id": 1, "v": [[1, 2]]}], msg="$type 'array' matches nested array") + + +SCALAR_TYPE_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="int64_scalar", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "v": Int64(2**53)}, {"_id": 2, "v": Int64(100)}), + filter={"v": Int64(2**53)}, + hint="wc_all", + expected=[{"_id": 1, "v": Int64(2**53)}], + msg="int64 scalar is queryable via wildcard", + ), + IndexQueryTestCase( + id="double_scalar", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "v": 3.14}, {"_id": 2, "v": 2.71}), + filter={"v": 3.14}, + hint="wc_all", + expected=[{"_id": 1, "v": 3.14}], + msg="double scalar is queryable via wildcard", + ), + IndexQueryTestCase( + id="decimal128_scalar", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "v": Decimal128("1.5")}, {"_id": 2, "v": Decimal128("2.5")}), + filter={"v": Decimal128("1.5")}, + hint="wc_all", + expected=[{"_id": 1, "v": Decimal128("1.5")}], + msg="Decimal128 scalar is queryable via wildcard", + ), + IndexQueryTestCase( + id="bool_true_scalar", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "v": True}, {"_id": 2, "v": False}), + filter={"v": True}, + hint="wc_all", + expected=[{"_id": 1, "v": True}], + msg="boolean true is queryable via wildcard", + ), + IndexQueryTestCase( + id="bool_false_scalar", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "v": True}, {"_id": 2, "v": False}), + filter={"v": False}, + hint="wc_all", + expected=[{"_id": 2, "v": False}], + msg="boolean false is queryable via wildcard", + ), + IndexQueryTestCase( + id="date_scalar", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "v": datetime(2020, 1, 1, tzinfo=timezone.utc)}, + {"_id": 2, "v": datetime(2021, 6, 15, tzinfo=timezone.utc)}, + ), + filter={"v": datetime(2020, 1, 1, tzinfo=timezone.utc)}, + hint="wc_all", + expected=[{"_id": 1, "v": datetime(2020, 1, 1, tzinfo=timezone.utc)}], + msg="date scalar is queryable via wildcard", + ), + IndexQueryTestCase( + id="objectid_scalar", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "v": ObjectId("507f1f77bcf86cd799439011")}, + {"_id": 2, "v": ObjectId("507f1f77bcf86cd799439012")}, + ), + filter={"v": ObjectId("507f1f77bcf86cd799439011")}, + hint="wc_all", + expected=[{"_id": 1, "v": ObjectId("507f1f77bcf86cd799439011")}], + msg="ObjectId scalar is queryable via wildcard", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SCALAR_TYPE_TESTS)) +def test_wildcard_scalar_types(collection, test): + """Verify wildcard index correctly indexes and retrieves scalar BSON types.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + result = execute_command( + collection, {"find": collection.name, "filter": test.filter, "hint": test.hint} + ) + assertSuccess(result, test.expected, msg=test.msg) + + +SPECIAL_TYPE_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="binary_type", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "v": Binary(b"\x01\x02\x03")}, + {"_id": 2, "v": Binary(b"\x04\x05\x06")}, + ), + filter={"v": Binary(b"\x01\x02\x03")}, + hint="wc_all", + expected=[{"_id": 1, "v": b"\x01\x02\x03"}], + msg="Binary data is queryable via wildcard", + ), + IndexQueryTestCase( + id="timestamp_type", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "v": Timestamp(1000, 1)}, + {"_id": 2, "v": Timestamp(2000, 1)}, + ), + filter={"v": Timestamp(1000, 1)}, + hint="wc_all", + expected=[{"_id": 1, "v": Timestamp(1000, 1)}], + msg="Timestamp is queryable via wildcard", + ), + IndexQueryTestCase( + id="minkey_type", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "v": MinKey()}, {"_id": 2, "v": 5}), + filter={"v": MinKey()}, + hint="wc_all", + expected=[{"_id": 1, "v": MinKey()}], + msg="MinKey is queryable via wildcard", + ), + IndexQueryTestCase( + id="maxkey_type", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "v": MaxKey()}, {"_id": 2, "v": 5}), + filter={"v": MaxKey()}, + hint="wc_all", + expected=[{"_id": 1, "v": MaxKey()}], + msg="MaxKey is queryable via wildcard", + ), + IndexQueryTestCase( + id="javascript_code_type", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "v": Code("function(){return 1;}")}, + {"_id": 2, "v": Code("function(){return 2;}")}, + ), + filter={"v": Code("function(){return 1;}")}, + hint="wc_all", + expected=[{"_id": 1, "v": Code("function(){return 1;}")}], + msg="JavaScript Code is queryable via wildcard", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SPECIAL_TYPE_TESTS)) +def test_wildcard_special_types(collection, test): + """Verify wildcard index correctly indexes and retrieves special BSON types.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + result = execute_command( + collection, {"find": collection.name, "filter": test.filter, "hint": test.hint} + ) + assertSuccess(result, test.expected, msg=test.msg) + + +OBJECT_ARRAY_TYPE_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="nested_object_leaf_query", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "obj": {"x": 1, "y": 2}}, + {"_id": 2, "obj": {"x": 3, "y": 4}}, + ), + filter={"obj.x": 1}, + hint="wc_all", + expected=[{"_id": 1, "obj": {"x": 1, "y": 2}}], + msg="Nested object leaf field is queryable via wildcard", + ), + IndexQueryTestCase( + id="deeply_nested_object", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "a": {"b": {"c": {"d": 10}}}}, + {"_id": 2, "a": {"b": {"c": {"d": 20}}}}, + ), + filter={"a.b.c.d": 10}, + hint="wc_all", + expected=[{"_id": 1, "a": {"b": {"c": {"d": 10}}}}], + msg="Deeply nested object leaf (3+ levels) is queryable via wildcard", + ), + IndexQueryTestCase( + id="array_scalar_elements", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "tags": ["red", "blue"]}, + {"_id": 2, "tags": ["green", "yellow"]}, + ), + filter={"tags": "blue"}, + hint="wc_all", + expected=[{"_id": 1, "tags": ["red", "blue"]}], + msg="Array scalar element match via wildcard", + ), + IndexQueryTestCase( + id="array_numeric_elements_range", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "scores": [10, 20, 30]}, + {"_id": 2, "scores": [40, 50]}, + ), + filter={"scores": {"$gte": 25}}, + hint="wc_all", + sort={"_id": 1}, + expected=[{"_id": 1, "scores": [10, 20, 30]}, {"_id": 2, "scores": [40, 50]}], + msg="Array numeric elements with range query via wildcard", + ), + IndexQueryTestCase( + id="array_of_objects_nested_field", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "items": [{"name": "a", "qty": 5}, {"name": "b", "qty": 10}]}, + {"_id": 2, "items": [{"name": "c", "qty": 15}]}, + ), + filter={"items.qty": 10}, + hint="wc_all", + expected=[{"_id": 1, "items": [{"name": "a", "qty": 5}, {"name": "b", "qty": 10}]}], + msg="Array of objects nested field is queryable via wildcard", + ), + IndexQueryTestCase( + id="mixed_type_field_int_and_string", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "v": 42}, + {"_id": 2, "v": "42"}, + {"_id": 3, "v": 42.0}, + ), + filter={"v": "42"}, + hint="wc_all", + expected=[{"_id": 2, "v": "42"}], + msg="String match does not return numeric equivalent", + ), + IndexQueryTestCase( + id="object_with_array_value", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "meta": {"tags": ["x", "y"], "count": 2}}, + {"_id": 2, "meta": {"tags": ["z"], "count": 1}}, + ), + filter={"meta.tags": "x"}, + hint="wc_all", + expected=[{"_id": 1, "meta": {"tags": ["x", "y"], "count": 2}}], + msg="Array inside object is queryable via wildcard", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(OBJECT_ARRAY_TYPE_TESTS)) +def test_wildcard_object_and_array_types(collection, test): + """Verify wildcard index correctly indexes and retrieves objects and arrays.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter, "hint": test.hint} + if test.sort: + cmd["sort"] = test.sort + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_errors.py b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_errors.py new file mode 100644 index 000000000..074d668af --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_errors.py @@ -0,0 +1,497 @@ +"""Tests for wildcard index error cases.""" + +import pytest + +from documentdb_tests.compatibility.tests.core.indexes.commands.utils.index_test_case import ( + IndexQueryTestCase, + IndexTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccess +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + CANNOT_CREATE_INDEX_ERROR, + CANNOT_USE_MIN_MAX_ERROR, + FAILED_TO_PARSE_ERROR, + FIELD_PATH_DOLLAR_PREFIX_ERROR, + INDEX_NOT_FOUND_ERROR, + INDEX_OPTIONS_CONFLICT_ERROR, + INVALID_INDEX_SPEC_OPTION_ERROR, + NO_QUERY_EXECUTION_PLANS_ERROR, + PROJECT_COMPUTED_FIELD_BANNED_ERROR, + PROJECT_EXCLUSION_IN_INCLUSION_ERROR, + WILDCARD_COMPOUND_PREFIX_MULTIKEY_ERROR, + WILDCARD_COMPOUND_PREFIX_NOT_EXCLUDED_ERROR, + WILDCARD_MULTIPLE_FIELDS_ERROR, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.index + + +INCOMPATIBLE_OPTION_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="unique", + indexes=({"key": {"$**": 1}, "name": "wc", "unique": True},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard index with unique:true should fail with CannotCreateIndex", + ), + IndexTestCase( + id="sparse", + indexes=({"key": {"$**": 1}, "name": "wc", "sparse": True},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard index with sparse:true should fail with CannotCreateIndex", + ), + IndexTestCase( + id="ttl_expireAfterSeconds", + indexes=({"key": {"$**": 1}, "name": "wc", "expireAfterSeconds": 3600},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard index with expireAfterSeconds should fail with CannotCreateIndex", + ), + IndexTestCase( + id="key_type_2d", + indexes=({"key": {"$**": "2d"}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard key with 2d type should fail with CannotCreateIndex", + ), + IndexTestCase( + id="key_type_2dsphere", + indexes=({"key": {"$**": "2dsphere"}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard key with 2dsphere type should fail with CannotCreateIndex", + ), + IndexTestCase( + id="key_type_hashed", + indexes=({"key": {"$**": "hashed"}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard key with hashed type should fail with CannotCreateIndex", + ), + IndexTestCase( + id="sort_order_zero", + indexes=({"key": {"$**": 0}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard key with sort order 0 should fail with CannotCreateIndex", + ), + IndexTestCase( + id="scoped_sort_order_zero", + indexes=({"key": {"sub.$**": 0}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Scoped wildcard key with sort order 0 should fail with CannotCreateIndex", + ), + IndexTestCase( + id="key_nan", + indexes=({"key": {"$**": float("nan")}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard key with NaN direction should fail with CannotCreateIndex", + ), + IndexTestCase( + id="index_version_v0", + indexes=({"key": {"$**": 1}, "name": "wc", "v": 0},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard index with v:0 should fail with CannotCreateIndex", + ), + IndexTestCase( + id="index_version_v1", + indexes=({"key": {"$**": 1}, "name": "wc", "v": 1},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard index with v:1 should fail with CannotCreateIndex", + ), + IndexTestCase( + id="string_key_wildcard_on_dollar", + indexes=({"key": {"$**": "wildcard"}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg='Wildcard key with string type "wildcard" should fail with CannotCreateIndex', + ), + IndexTestCase( + id="string_key_hello", + indexes=({"key": {"$**": "hello"}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard key with arbitrary string type should fail with CannotCreateIndex", + ), + IndexTestCase( + id="chained_wildcard_specifier", + indexes=({"key": {"a.$**.$**": 1}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Chained wildcard specifier a.$**.$** should fail with CannotCreateIndex", + ), + IndexTestCase( + id="double_wildcard_specifier", + indexes=({"key": {"$**.$**": 1}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Double wildcard specifier $**.$** should fail with CannotCreateIndex", + ), + IndexTestCase( + id="two_wildcard_terms_in_compound", + indexes=({"key": {"$**": 1, "sub.$**": 1}, "name": "cwi_two_wc"},), + expected=WILDCARD_MULTIPLE_FIELDS_ERROR, + msg="Two wildcard terms should fail with WildcardMultipleFields", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INCOMPATIBLE_OPTION_TESTS)) +def test_wildcard_create_incompatible_option_fails(collection, test): + """Verify incompatible wildcard index options/types are rejected.""" + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + assertFailureCode(result, test.expected, msg=test.msg) + + +BAD_SPECIFIER_PLACEMENT_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="wildcard_with_continuation", + indexes=({"key": {"$**.a": 1}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard specifier with continuation $**.a should fail with CannotCreateIndex", + ), + IndexTestCase( + id="wildcard_not_final_component", + indexes=({"key": {"a.$**.b": 1}, "name": "wc"},), + expected=CANNOT_CREATE_INDEX_ERROR, + msg="Wildcard specifier not final component should fail with CannotCreateIndex", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(BAD_SPECIFIER_PLACEMENT_TESTS)) +def test_wildcard_create_bad_specifier_placement_fails(collection, test): + """Verify mis-placed wildcard specifiers are rejected with CannotCreateIndex.""" + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + assertFailureCode(result, test.expected, msg=test.msg) + + +INVALID_PROJECTION_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="mixed_inclusion_exclusion", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"a": 1, "b": 0}},), + expected=PROJECT_EXCLUSION_IN_INCLUSION_ERROR, + msg="Mixed inclusion/exclusion (no _id exception) should fail with 31254", + ), + IndexTestCase( + id="empty_projection", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {}},), + expected=FAILED_TO_PARSE_ERROR, + msg="Empty wildcardProjection should fail with FailedToParse", + ), + IndexTestCase( + id="field_path_key_with_projection", + indexes=({"key": {"sub.$**": 1}, "name": "wc", "wildcardProjection": {"a": 1}},), + expected=FAILED_TO_PARSE_ERROR, + msg="Field-path wildcard key with wildcardProjection should fail with FailedToParse", + ), + IndexTestCase( + id="non_wildcard_index_with_projection", + indexes=({"key": {"a": 1}, "name": "reg", "wildcardProjection": {"a": 1}},), + expected=BAD_VALUE_ERROR, + msg="Non-wildcard index with wildcardProjection should fail with BadValue", + ), + IndexTestCase( + id="value_string", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"a": "yes"}},), + expected=PROJECT_COMPUTED_FIELD_BANNED_ERROR, + msg="wildcardProjection with a string value is parsed as a computed field and rejected", + ), + IndexTestCase( + id="value_numeric_string", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"a": "1"}},), + expected=PROJECT_COMPUTED_FIELD_BANNED_ERROR, + msg="wildcardProjection with a numeric-string value is a computed field and rejected", + ), + IndexTestCase( + id="value_array", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"a": [1]}},), + expected=PROJECT_COMPUTED_FIELD_BANNED_ERROR, + msg="wildcardProjection with an array value is a computed field and rejected", + ), + IndexTestCase( + id="value_null", + indexes=({"key": {"$**": 1}, "name": "wc", "wildcardProjection": {"a": None}},), + expected=PROJECT_COMPUTED_FIELD_BANNED_ERROR, + msg="wildcardProjection with a null value is a computed field and rejected", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(INVALID_PROJECTION_TESTS)) +def test_wildcard_projection_invalid(collection, test): + """Verify invalid wildcardProjection configurations are rejected with the correct code.""" + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + assertFailureCode(result, test.expected, msg=test.msg) + + +MIN_MAX_HINT_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="min", + command_options={"min": {"a": 2}}, + msg="min with wildcard hint -> 51174", + ), + IndexTestCase( + id="max", + command_options={"max": {"a": 4}}, + msg="max with wildcard hint -> 51174", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(MIN_MAX_HINT_TESTS)) +def test_wildcard_hint_min_max_fails(collection, test): + """Using min/max bounds with a wildcard index hint fails with error 51174.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + collection.insert_many([{"_id": 1, "a": 1}, {"_id": 2, "a": 5}]) + cmd = {"find": collection.name, "hint": {"$**": 1}, **test.command_options} + result = execute_command(collection, cmd) + assertFailureCode(result, CANNOT_USE_MIN_MAX_ERROR, msg=test.msg) + + +HINT_REJECTED_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="inclusion_non_projected_field", + indexes=({"key": {"$**": 1}, "name": "wc_inc", "wildcardProjection": {"a": 1}},), + doc=({"_id": 1, "a": 1, "b": 2},), + filter={"b": 2}, + hint="wc_inc", + expected=NO_QUERY_EXECUTION_PLANS_ERROR, + msg="Hinting wildcard for a non-projected field should be rejected", + ), + IndexQueryTestCase( + id="exclusion_excluded_field", + indexes=({"key": {"$**": 1}, "name": "wc_exc", "wildcardProjection": {"a": 0}},), + doc=({"_id": 1, "a": 1, "b": 2},), + filter={"a": 1}, + hint="wc_exc", + expected=NO_QUERY_EXECUTION_PLANS_ERROR, + msg="Hinting wildcard for an excluded field should be rejected", + ), + IndexQueryTestCase( + id="scoped_path_not_in_query", + indexes=({"key": {"sub.$**": 1}, "name": "wc_sub"},), + doc=({"_id": 1, "other": 1, "sub": {"x": 1}},), + filter={"other": 1}, + hint="wc_sub", + expected=NO_QUERY_EXECUTION_PLANS_ERROR, + msg="Hint scoped wildcard on out-of-scope path -> NoQueryExecutionPlans", + ), + IndexQueryTestCase( + id="id_excluded_from_default_wildcard", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 2}), + filter={"_id": 2}, + hint="wc_all", + expected=NO_QUERY_EXECUTION_PLANS_ERROR, + msg="Hinting default wildcard index for _id query should be rejected", + ), + IndexQueryTestCase( + id="dollar_path_projection", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 5},), + filter={"a": 5}, + hint="wc_all", + command_options={"projection": {"$_path": 1}}, + expected=FIELD_PATH_DOLLAR_PREFIX_ERROR, + msg="Projecting $_path is rejected with 16410", + ), + IndexQueryTestCase( + id="nonexistent_index", + indexes=(), + doc=({"_id": 1, "a": 1},), + filter={"a": 1}, + hint={"$**": 1}, + expected=BAD_VALUE_ERROR, + msg="Hint on non-existent wildcard index -> BadValue", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(HINT_REJECTED_TESTS)) +def test_wildcard_hint_rejected(collection, test): + """Verify wildcard index hint rejection cases.""" + if test.indexes: + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter, "hint": test.hint} + if test.command_options: + cmd.update(test.command_options) + result = execute_command(collection, cmd) + assertFailureCode(result, test.expected, msg=test.msg) + + +def test_wildcard_create_failure_leaves_no_index(collection): + """A failed wildcard index creation leaves no extra index on the collection.""" + collection.insert_one({"_id": 1, "a": 1}) + execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"$**": 1}, "name": "wc", "unique": True}], + }, + ) + result = execute_command(collection, {"listIndexes": collection.name}) + assertSuccess( + result, + ["_id_"], + transform=lambda batch: sorted(idx["name"] for idx in batch), + msg="Failed wildcard index creation should leave only the _id index", + ) + + +def test_clustered_index_with_wildcard_key_fails(collection): + """Creating a clustered collection whose clustered index key uses wildcard syntax fails.""" + name = f"{collection.name}_bad_clustered" + result = execute_command( + collection, + {"create": name, "clusteredIndex": {"key": {"$**": 1}, "unique": True}}, + ) + assertFailureCode( + result, + INVALID_INDEX_SPEC_OPTION_ERROR, + msg="Clustered index with wildcard key should fail with InvalidIndexSpecificationOption", + ) + + +def test_wildcard_text_query_no_text_index_fails(collection): + """A $text query on a collection with only a wildcard index fails (no text index).""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + collection.insert_many([{"_id": 1, "t": "hello world"}]) + result = execute_command( + collection, {"find": collection.name, "filter": {"$text": {"$search": "hello"}}} + ) + assertFailureCode( + result, INDEX_NOT_FOUND_ERROR, msg="$text with only a wildcard index should fail" + ) + + +def test_compound_wildcard_prefix_multikey_insert_fails(collection): + """Inserting a doc whose non-wildcard prefix field is an array fails: a compound wildcard + index's non-wildcard prefix cannot be multikey.""" + execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"a": 1, "$**": 1}, "name": "cwi", "wildcardProjection": {"a": 0}}], + }, + ) + result = execute_command( + collection, + {"insert": collection.name, "documents": [{"_id": 1, "a": [1, 2], "b": 5}]}, + ) + assertFailureCode( + result, + WILDCARD_COMPOUND_PREFIX_MULTIKEY_ERROR, + msg="Inserting an array into the compound wildcard prefix field should fail with 7246301", + ) + + +def test_compound_wildcard_prefix_multikey_create_fails(collection): + """Creating a compound wildcard index fails when existing data already has an array in the + non-wildcard prefix field.""" + collection.insert_one({"_id": 1, "a": [1, 2], "b": 5}) + result = execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"a": 1, "$**": 1}, "name": "cwi", "wildcardProjection": {"a": 0}}], + }, + ) + assertFailureCode( + result, + WILDCARD_COMPOUND_PREFIX_MULTIKEY_ERROR, + msg="Creating a compound wildcard over an existing multikey prefix " + "should fail with 7246301", + ) + + +def test_compound_wildcard_prefix_multikey_update_fails(collection): + """Updating the non-wildcard prefix field to an array fails after a compound wildcard index + exists.""" + execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"a": 1, "$**": 1}, "name": "cwi", "wildcardProjection": {"a": 0}}], + }, + ) + collection.insert_one({"_id": 1, "a": 1, "b": 5}) + result = execute_command( + collection, + { + "update": collection.name, + "updates": [{"q": {"_id": 1}, "u": {"$set": {"a": [1, 2]}}}], + }, + ) + assertFailureCode( + result, + WILDCARD_COMPOUND_PREFIX_MULTIKEY_ERROR, + msg="Updating the compound wildcard prefix field to an array should fail with 7246301", + ) + + +def test_compound_wildcard_overlapping_field_not_excluded_fails(collection): + """A compound wildcard index whose regular key field also falls in wildcard scope must + exclude that field via wildcardProjection. Providing a projection that does not exclude the + overlapping field fails: the wildcardProjection must exclude all regular index fields.""" + result = execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [ + { + "key": {"a": 1, "$**": 1}, + "name": "cwi", + # 'a' is a regular key field and remains in wildcard scope (only 'b' excluded). + "wildcardProjection": {"b": 0}, + } + ], + }, + ) + assertFailureCode( + result, + WILDCARD_COMPOUND_PREFIX_NOT_EXCLUDED_ERROR, + msg="Compound wildcard whose projection does not exclude the overlapping regular key " + "field should fail with 7246209", + ) + + +def test_two_compound_wildcards_same_spec_different_name_conflicts(collection): + """Two compound wildcard indexes with the same prefix and identical wildcard scope + (same key and wildcardProjection) but different names conflict with IndexOptionsConflict.""" + execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [ + {"key": {"a": 1, "$**": 1}, "name": "cwi_first", "wildcardProjection": {"a": 0}} + ], + }, + ) + result = execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [ + {"key": {"a": 1, "$**": 1}, "name": "cwi_second", "wildcardProjection": {"a": 0}} + ], + }, + ) + assertFailureCode( + result, + INDEX_OPTIONS_CONFLICT_ERROR, + msg="Identical compound wildcard spec under a different name should fail with " + "IndexOptionsConflict", + ) diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_query.py b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_query.py new file mode 100644 index 000000000..c877ffa10 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_query.py @@ -0,0 +1,870 @@ +"""Tests for wildcard index query behavior.""" + +import pytest + +from documentdb_tests.compatibility.tests.core.indexes.commands.utils.index_test_case import ( + IndexQueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess, assertSuccessPartial +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.index + +EQUALITY_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="scalar", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 5}, {"_id": 2, "a": 7}), + filter={"a": 5}, + hint="wc_all", + expected=[{"_id": 1, "a": 5}], + msg="Equality on scalar via wildcard index", + ), + IndexQueryTestCase( + id="string", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "s": "hello"}, {"_id": 2, "s": "world"}), + filter={"s": "world"}, + hint="wc_all", + expected=[{"_id": 2, "s": "world"}], + msg="Equality on string via wildcard index", + ), + IndexQueryTestCase( + id="nested_path", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": {"b": 1}}, {"_id": 2, "a": {"b": 2}}), + filter={"a.b": 2}, + hint="wc_all", + expected=[{"_id": 2, "a": {"b": 2}}], + msg="Equality on nested path via wildcard", + ), + IndexQueryTestCase( + id="no_match", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 5}, {"_id": 2, "a": 7}), + filter={"a": 99}, + hint="wc_all", + expected=[], + msg="No-match equality returns empty set", + ), + IndexQueryTestCase( + id="schema_less_field", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "b": "x"}, {"_id": 3, "a": 1, "c": True}), + filter={"b": "x"}, + hint="wc_all", + expected=[{"_id": 2, "b": "x"}], + msg="Query on a field only some documents have returns the correct subset", + ), + IndexQueryTestCase( + id="scoped_in_scope", + indexes=({"key": {"sub.$**": 1}, "name": "wc_sub"},), + doc=({"_id": 1, "sub": {"x": 1}}, {"_id": 2, "sub": {"x": 2}}), + filter={"sub.x": 2}, + hint="wc_sub", + expected=[{"_id": 2, "sub": {"x": 2}}], + msg="Scoped wildcard in-scope query", + ), + IndexQueryTestCase( + id="hint_by_key_pattern", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 2}), + filter={"a": 2}, + hint={"$**": 1}, + expected=[{"_id": 2, "a": 2}], + msg="Hint by key pattern returns correct doc", + ), + IndexQueryTestCase( + id="id_field_unhinted", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 2}), + filter={"_id": 2}, + expected=[{"_id": 2, "a": 2}], + msg="Query on _id returns the correct document with only a wildcard index", + ), + IndexQueryTestCase( + id="id_included_via_projection_hinted", + indexes=({"key": {"$**": 1}, "name": "wc_id", "wildcardProjection": {"_id": 1}},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 2}), + filter={"_id": 2}, + hint="wc_id", + expected=[{"_id": 2, "a": 2}], + msg="Hinted _id query works when _id included in wildcardProjection", + ), + IndexQueryTestCase( + id="hidden_index_unhinted", + indexes=({"key": {"$**": 1}, "name": "wc_hidden", "hidden": True},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 2}), + filter={"a": 2}, + expected=[{"_id": 2, "a": 2}], + msg="Query correctness when the only wildcard index is hidden", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(EQUALITY_TESTS)) +def test_wildcard_equality(collection, test): + """Verify equality queries return correct documents via wildcard indexes.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter} + if test.hint: + cmd["hint"] = test.hint + if test.sort: + cmd["sort"] = test.sort + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +RANGE_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="gt", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 9}), + filter={"a": {"$gt": 4}}, + hint="wc_all", + sort={"_id": 1}, + expected=[{"_id": 2, "a": 5}, {"_id": 3, "a": 9}], + msg="$gt range via wildcard", + ), + IndexQueryTestCase( + id="gte", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 9}), + filter={"a": {"$gte": 5}}, + hint="wc_all", + sort={"_id": 1}, + expected=[{"_id": 2, "a": 5}, {"_id": 3, "a": 9}], + msg="$gte range via wildcard", + ), + IndexQueryTestCase( + id="lt", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 9}), + filter={"a": {"$lt": 5}}, + hint="wc_all", + sort={"_id": 1}, + expected=[{"_id": 1, "a": 1}], + msg="$lt range via wildcard", + ), + IndexQueryTestCase( + id="lte", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 9}), + filter={"a": {"$lte": 5}}, + hint="wc_all", + sort={"_id": 1}, + expected=[{"_id": 1, "a": 1}, {"_id": 2, "a": 5}], + msg="$lte range via wildcard", + ), + IndexQueryTestCase( + id="type_bracketing", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "a": 10}, + {"_id": 2, "a": "20"}, + {"_id": 3, "a": True}, + {"_id": 4, "a": 30}, + ), + filter={"a": {"$gt": 5}}, + hint="wc_all", + sort={"_id": 1}, + expected=[{"_id": 1, "a": 10}, {"_id": 4, "a": 30}], + msg="Numeric range is type-bracketed — excludes string/bool values", + ), + IndexQueryTestCase( + id="scoped_range_in_scope", + indexes=({"key": {"sub.$**": 1}, "name": "wc_sub"},), + doc=( + {"_id": 1, "sub": {"x": 1}}, + {"_id": 2, "sub": {"x": 5}}, + {"_id": 3, "sub": {"x": 9}}, + ), + filter={"sub.x": {"$gte": 5}}, + hint="wc_sub", + sort={"_id": 1}, + expected=[{"_id": 2, "sub": {"x": 5}}, {"_id": 3, "sub": {"x": 9}}], + msg="Scoped wildcard range in-scope query", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(RANGE_TESTS)) +def test_wildcard_range(collection, test): + """Verify range queries return only documents satisfying the predicate.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter, "hint": test.hint, "sort": test.sort} + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +SCOPED_WILDCARD_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="deeper_scope_nested_leaf", + indexes=({"key": {"a.b.$**": 1}, "name": "wc_ab"},), + doc=({"_id": 1, "a": {"b": {"c": 1}}}, {"_id": 2, "a": {"b": {"c": 2}}}), + filter={"a.b.c": 2}, + hint="wc_ab", + expected=[{"_id": 2, "a": {"b": {"c": 2}}}], + msg="Deeper scoped wildcard (a.b.$**) serves a query on a leaf within scope", + ), + IndexQueryTestCase( + id="scope_sibling_leaf", + indexes=({"key": {"sub.$**": 1}, "name": "wc_sub"},), + doc=({"_id": 1, "sub": {"x": 1, "y": 2}}, {"_id": 2, "sub": {"x": 3, "y": 4}}), + filter={"sub.y": 2}, + hint="wc_sub", + expected=[{"_id": 1, "sub": {"x": 1, "y": 2}}], + msg="Scoped wildcard indexes every leaf under scope; a sibling leaf is queryable", + ), + IndexQueryTestCase( + id="scope_multikey_array", + indexes=({"key": {"sub.$**": 1}, "name": "wc_sub"},), + doc=({"_id": 1, "sub": {"tags": ["a", "b"]}}, {"_id": 2, "sub": {"tags": ["c"]}}), + filter={"sub.tags": "b"}, + hint="wc_sub", + expected=[{"_id": 1, "sub": {"tags": ["a", "b"]}}], + msg="Scoped wildcard matches an array element within scope (multikey)", + ), + IndexQueryTestCase( + id="scope_in_predicate", + indexes=({"key": {"sub.$**": 1}, "name": "wc_sub"},), + doc=( + {"_id": 1, "sub": {"x": 1}}, + {"_id": 2, "sub": {"x": 2}}, + {"_id": 3, "sub": {"x": 3}}, + ), + filter={"sub.x": {"$in": [1, 3]}}, + hint="wc_sub", + sort={"_id": 1}, + expected=[{"_id": 1, "sub": {"x": 1}}, {"_id": 3, "sub": {"x": 3}}], + msg="Scoped wildcard serves an $in predicate on an in-scope field", + ), + IndexQueryTestCase( + id="scope_sort_within_scope", + indexes=({"key": {"sub.$**": 1}, "name": "wc_sub"},), + doc=( + {"_id": 1, "sub": {"x": 9}}, + {"_id": 2, "sub": {"x": 3}}, + {"_id": 3, "sub": {"x": 6}}, + ), + filter={"sub.x": {"$gte": 3}}, + sort={"sub.x": 1}, + hint="wc_sub", + expected=[ + {"_id": 2, "sub": {"x": 3}}, + {"_id": 3, "sub": {"x": 6}}, + {"_id": 1, "sub": {"x": 9}}, + ], + msg="Scoped wildcard provides sort order for an in-scope field", + ), + IndexQueryTestCase( + id="scope_rooted_at_id_subfield", + indexes=({"key": {"_id.$**": 1}, "name": "wc_id_sub"},), + doc=({"_id": {"a": 1, "b": 2}}, {"_id": {"a": 3, "b": 4}}), + filter={"_id.a": 3}, + hint="wc_id_sub", + expected=[{"_id": {"a": 3, "b": 4}}], + msg="Scoped wildcard rooted at _id serves a query on an _id subfield", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SCOPED_WILDCARD_TESTS)) +def test_wildcard_scoped_path(collection, test): + """Verify path-specific (scoped) wildcard indexes serve queries on fields within scope.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter} + if test.hint: + cmd["hint"] = test.hint + if test.sort: + cmd["sort"] = test.sort + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +OPERATOR_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="or_two_fields", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "a": 1, "b": 0}, + {"_id": 2, "a": 0, "b": 2}, + {"_id": 3, "a": 0, "b": 0}, + ), + filter={"$or": [{"a": 1}, {"b": 2}]}, + hint="wc_all", + sort={"_id": 1}, + expected=[{"_id": 1, "a": 1, "b": 0}, {"_id": 2, "a": 0, "b": 2}], + msg="$or across two fields returns the correct union", + ), + IndexQueryTestCase( + id="or_multi_field_branch", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "a": 1, "b": 2}, + {"_id": 2, "a": 1, "b": 9}, + {"_id": 3, "c": 3}, + ), + filter={"$or": [{"a": 1, "b": 2}, {"c": 3}]}, + sort={"_id": 1}, + expected=[{"_id": 1, "a": 1, "b": 2}, {"_id": 3, "c": 3}], + msg="$or with a multi-field branch returns correct results", + ), + IndexQueryTestCase( + id="explicit_and", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "a": 1, "b": 2}, + {"_id": 2, "a": 1, "b": 9}, + {"_id": 3, "a": 5, "b": 2}, + ), + filter={"$and": [{"a": 1}, {"b": 2}]}, + hint="wc_all", + expected=[{"_id": 1, "a": 1, "b": 2}], + msg="Explicit $and across two fields returns the correct intersection", + ), + IndexQueryTestCase( + id="implicit_and", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "a": 1, "b": 5}, + {"_id": 2, "a": 1, "b": 9}, + {"_id": 3, "a": 2, "b": 5}, + ), + filter={"a": 1, "b": 5}, + hint="wc_all", + expected=[{"_id": 1, "a": 1, "b": 5}], + msg="Implicit AND applies both predicates via wildcard hint", + ), + IndexQueryTestCase( + id="elemmatch_not_covered", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "a": [{"b": 5}, {"b": 1}]}, + {"_id": 2, "a": [{"b": 1}]}, + ), + filter={"a": {"$elemMatch": {"b": {"$gte": 4}}}}, + hint="wc_all", + expected=[{"_id": 1, "a": [{"b": 5}, {"b": 1}]}], + msg="$elemMatch (not coverable) returns correct docs", + ), + IndexQueryTestCase( + id="prefix_regex", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "s": "hello"}, + {"_id": 2, "s": "help"}, + {"_id": 3, "s": "world"}, + ), + filter={"s": {"$regex": "^he"}}, + hint="wc_all", + expected=[{"_id": 1, "s": "hello"}, {"_id": 2, "s": "help"}], + msg="Prefix $regex on a string field served by wildcard index", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(OPERATOR_TESTS)) +def test_wildcard_operators(collection, test): + """Verify logical and array operator queries return correct documents via the wildcard + index.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter} + if test.hint: + cmd["hint"] = test.hint + if test.sort: + cmd["sort"] = test.sort + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +SORT_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="same_field_ascending", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 9}, {"_id": 2, "a": 3}, {"_id": 3, "a": 6}), + filter={"a": {"$gte": 3}}, + sort={"a": 1}, + hint="wc_all", + expected=[{"_id": 2, "a": 3}, {"_id": 3, "a": 6}, {"_id": 1, "a": 9}], + msg="Ascending sort on the query field returns ordered results", + ), + IndexQueryTestCase( + id="same_field_descending", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 9}, {"_id": 2, "a": 3}, {"_id": 3, "a": 6}), + filter={"a": {"$gte": 3}}, + sort={"a": -1}, + hint="wc_all", + expected=[{"_id": 1, "a": 9}, {"_id": 3, "a": 6}, {"_id": 2, "a": 3}], + msg="Descending sort on the query field returns ordered results", + ), + IndexQueryTestCase( + id="different_field", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=( + {"_id": 1, "a": 1, "b": 30}, + {"_id": 2, "a": 1, "b": 10}, + {"_id": 3, "a": 1, "b": 20}, + ), + filter={"a": 1}, + sort={"b": 1}, + hint="wc_all", + expected=[ + {"_id": 2, "a": 1, "b": 10}, + {"_id": 3, "a": 1, "b": 20}, + {"_id": 1, "a": 1, "b": 30}, + ], + msg="Sort on a different field returns correctly ordered results", + ), + IndexQueryTestCase( + id="multikey_field", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": [3, 1]}, {"_id": 2, "a": [2, 5]}, {"_id": 3, "a": [0, 4]}), + filter={"a": {"$gte": 0}}, + sort={"a": 1}, + hint="wc_all", + expected=[{"_id": 3, "a": [0, 4]}, {"_id": 1, "a": [3, 1]}, {"_id": 2, "a": [2, 5]}], + msg="Sort on multikey field orders by minimum element", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SORT_TESTS)) +def test_wildcard_sort(collection, test): + """Verify sorted queries via the wildcard index return correctly ordered results.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter, "sort": test.sort, "hint": test.hint} + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +PROJECTION_OUTPUT_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="equality", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 5}, {"_id": 2, "a": 7}), + filter={"a": 5}, + hint="wc_all", + expected=[{"a": 5}], + msg="Projection onto the query field returns only the projected field (equality)", + ), + IndexQueryTestCase( + id="range", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 9}), + filter={"a": {"$gte": 5}}, + hint="wc_all", + sort={"a": 1}, + expected=[{"a": 5}, {"a": 9}], + msg="Projection onto the query field returns only the projected field (range)", + ), + IndexQueryTestCase( + id="in", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 2}, {"_id": 3, "a": 3}), + filter={"a": {"$in": [1, 3]}}, + hint="wc_all", + sort={"a": 1}, + expected=[{"a": 1}, {"a": 3}], + msg="Projection onto the query field returns only the projected field ($in)", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(PROJECTION_OUTPUT_TESTS)) +def test_wildcard_projection_output(collection, test): + """Verify a hinted wildcard query with a projection onto the query field (_id excluded) + returns only the projected field. This checks projection output correctness, not that the + plan is index-covered (verifying coverage would require explain, which the suite does not + test).""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = { + "find": collection.name, + "filter": test.filter, + "projection": {"_id": 0, "a": 1}, + "hint": test.hint, + } + if test.sort: + cmd["sort"] = test.sort + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +WILDCARD_PROJECTION_QUERY_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="inclusion_nested_leaf_served", + indexes=({"key": {"$**": 1}, "name": "wc_inc", "wildcardProjection": {"a": 1}},), + doc=( + {"_id": 1, "a": {"b": 1}, "z": 9}, + {"_id": 2, "a": {"b": 2}, "z": 8}, + ), + filter={"a.b": 2}, + hint="wc_inc", + expected=[{"_id": 2, "a": {"b": 2}, "z": 8}], + msg="Inclusion projection indexes the whole subtree; a nested leaf under it is served", + ), + IndexQueryTestCase( + id="inclusion_range_on_projected_field", + indexes=({"key": {"$**": 1}, "name": "wc_inc", "wildcardProjection": {"a": 1}},), + doc=( + {"_id": 1, "a": 1, "b": 100}, + {"_id": 2, "a": 5, "b": 100}, + {"_id": 3, "a": 9, "b": 100}, + ), + filter={"a": {"$gte": 5}}, + hint="wc_inc", + sort={"_id": 1}, + expected=[{"_id": 2, "a": 5, "b": 100}, {"_id": 3, "a": 9, "b": 100}], + msg="Range query on an included field is served by the wildcard index", + ), + IndexQueryTestCase( + id="exclusion_sibling_of_excluded_served", + indexes=({"key": {"$**": 1}, "name": "wc_exc", "wildcardProjection": {"a": 0}},), + doc=( + {"_id": 1, "a": 1, "b": {"c": 1}}, + {"_id": 2, "a": 2, "b": {"c": 2}}, + ), + filter={"b.c": 2}, + hint="wc_exc", + expected=[{"_id": 2, "a": 2, "b": {"c": 2}}], + msg="Exclusion projection still indexes non-excluded nested leaves", + ), + IndexQueryTestCase( + id="exclusion_multiple_fields_served", + indexes=({"key": {"$**": 1}, "name": "wc_exc2", "wildcardProjection": {"a": 0, "b": 0}},), + doc=( + {"_id": 1, "a": 1, "b": 2, "c": 3}, + {"_id": 2, "a": 4, "b": 5, "c": 6}, + ), + filter={"c": 6}, + hint="wc_exc2", + expected=[{"_id": 2, "a": 4, "b": 5, "c": 6}], + msg="Query on a field outside a multi-field exclusion projection is served", + ), + IndexQueryTestCase( + id="exclude_id_inclusion_serves_field", + indexes=( + { + "key": {"$**": 1}, + "name": "wc_noid", + "wildcardProjection": {"_id": 0, "a": 1}, + }, + ), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 2}), + filter={"a": 2}, + hint="wc_noid", + expected=[{"_id": 2, "a": 2}], + msg="Inclusion projection with _id excluded still serves the included field", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(WILDCARD_PROJECTION_QUERY_TESTS)) +def test_wildcard_projection_query_served(collection, test): + """Verify queries are served by a wildcard index constrained by a wildcardProjection.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter, "hint": test.hint} + if test.sort: + cmd["sort"] = test.sort + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +COMPOUND_WILDCARD_QUERY_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="prefix_and_wildcard_field_nonwildcard_first", + indexes=({"key": {"a": 1, "$**": 1}, "name": "cwi_a_wc", "wildcardProjection": {"a": 0}},), + doc=( + {"_id": 1, "a": 1, "b": 10}, + {"_id": 2, "a": 1, "b": 20}, + {"_id": 3, "a": 2, "b": 10}, + ), + filter={"a": 1, "b": 10}, + hint="cwi_a_wc", + expected=[{"_id": 1, "a": 1, "b": 10}], + msg="Compound wildcard (non-wildcard first) serves a prefix + wildcard-field query", + ), + IndexQueryTestCase( + id="prefix_and_wildcard_field_wildcard_first", + indexes=({"key": {"$**": 1, "a": 1}, "name": "cwi_wc_a", "wildcardProjection": {"a": 0}},), + doc=( + {"_id": 1, "a": 1, "b": 10}, + {"_id": 2, "a": 1, "b": 20}, + {"_id": 3, "a": 2, "b": 10}, + ), + filter={"a": 1, "b": 10}, + hint="cwi_wc_a", + expected=[{"_id": 1, "a": 1, "b": 10}], + msg="Compound wildcard (wildcard first) serves a prefix + wildcard-field query", + ), + IndexQueryTestCase( + id="prefix_equality_wildcard_range", + indexes=({"key": {"a": 1, "$**": 1}, "name": "cwi_a_wc", "wildcardProjection": {"a": 0}},), + doc=( + {"_id": 1, "a": 1, "b": 5}, + {"_id": 2, "a": 1, "b": 15}, + {"_id": 3, "a": 2, "b": 15}, + ), + filter={"a": 1, "b": {"$gte": 10}}, + hint="cwi_a_wc", + sort={"_id": 1}, + expected=[{"_id": 2, "a": 1, "b": 15}], + msg="Compound wildcard serves prefix equality plus a range on the wildcard field", + ), + IndexQueryTestCase( + id="prefix_only_query", + indexes=({"key": {"a": 1, "$**": 1}, "name": "cwi_a_wc", "wildcardProjection": {"a": 0}},), + doc=( + {"_id": 1, "a": 1, "b": 10}, + {"_id": 2, "a": 1, "b": 20}, + {"_id": 3, "a": 2, "b": 30}, + ), + filter={"a": 1}, + hint="cwi_a_wc", + sort={"_id": 1}, + expected=[{"_id": 1, "a": 1, "b": 10}, {"_id": 2, "a": 1, "b": 20}], + msg="Compound wildcard serves a query on the non-wildcard prefix field alone", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(COMPOUND_WILDCARD_QUERY_TESTS)) +def test_compound_wildcard_query(collection, test): + """Verify a compound wildcard index serves a query filtering on the non-wildcard prefix + field together with a wildcard-covered field.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + result = execute_command( + collection, + {"find": collection.name, "filter": test.filter, "hint": test.hint}, + ) + assertSuccess(result, test.expected, msg=test.msg) + + +COUNT_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="range", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 9}), + filter={"a": {"$gte": 5}}, + hint="wc_all", + expected={"n": 2, "ok": 1.0}, + msg="count range predicate", + ), + IndexQueryTestCase( + id="in_predicate", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 2}, {"_id": 3, "a": 3}, {"_id": 4, "a": 4}), + filter={"a": {"$in": [1, 3]}}, + hint="wc_all", + expected={"n": 2, "ok": 1.0}, + msg="count $in predicate", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(COUNT_TESTS)) +def test_wildcard_count(collection, test): + """Verify count returns the correct number of matches via the wildcard index.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + result = execute_command( + collection, {"count": collection.name, "query": test.filter, "hint": test.hint} + ) + assertSuccessPartial(result, test.expected, msg=test.msg) + + +AGGREGATION_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="match", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 9}), + command_options={"pipeline": [{"$match": {"a": {"$gte": 5}}}, {"$sort": {"_id": 1}}]}, + hint="wc_all", + expected=[{"_id": 2, "a": 5}, {"_id": 3, "a": 9}], + msg="$match via wildcard index", + ), + IndexQueryTestCase( + id="match_project_covered", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 5}, {"_id": 2, "a": 7}), + command_options={"pipeline": [{"$match": {"a": 5}}, {"$project": {"_id": 0, "a": 1}}]}, + hint="wc_all", + expected=[{"a": 5}], + msg="$match + $project returns projected fields", + ), + IndexQueryTestCase( + id="match_expr", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 9}), + command_options={ + "pipeline": [{"$match": {"$expr": {"$gt": ["$a", 4]}}}, {"$sort": {"_id": 1}}] + }, + expected=[{"_id": 2, "a": 5}, {"_id": 3, "a": 9}], + msg="$match with $expr returns correct docs", + ), + IndexQueryTestCase( + id="count", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 9}), + command_options={"pipeline": [{"$match": {"a": {"$gte": 5}}}, {"$count": "total"}]}, + hint="wc_all", + expected=[{"total": 2}], + msg="$count aggregation range predicate", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(AGGREGATION_TESTS)) +def test_wildcard_aggregation(collection, test): + """Verify aggregation pipelines on wildcard-indexed fields return correct results.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = { + "aggregate": collection.name, + "pipeline": test.command_options["pipeline"], + "cursor": {}, + } + if test.hint: + cmd["hint"] = test.hint + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +def _sorted_values(result): + return sorted(result["values"]) + + +DISTINCT_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="no_query", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 2}, {"_id": 3, "a": 2}, {"_id": 4, "a": 3}), + command_options={"key": "a"}, + expected=[1, 2, 3], + msg="distinct with no query", + ), + IndexQueryTestCase( + id="equality_query", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": 5}, {"_id": 2, "a": 5}, {"_id": 3, "a": 7}), + command_options={"key": "a", "query": {"a": 5}}, + expected=[5], + msg="distinct with equality query", + ), + IndexQueryTestCase( + id="multikey", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": [1, 2]}, {"_id": 2, "a": [2, 3]}, {"_id": 3, "a": []}), + command_options={"key": "a", "query": {"a": {"$gte": 1}}}, + expected=[1, 2, 3], + msg="distinct on multikey field unwraps array elements", + ), + IndexQueryTestCase( + id="dotted_path", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": {"b": 10}}, {"_id": 2, "a": {"b": 10}}, {"_id": 3, "a": {"b": 20}}), + command_options={"key": "a.b", "query": {"a.b": {"$gte": 10}}}, + expected=[10, 20], + msg="distinct on dotted path", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(DISTINCT_TESTS)) +def test_wildcard_distinct(collection, test): + """Verify distinct returns the correct unique values via the wildcard index.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + result = execute_command(collection, {"distinct": collection.name, **test.command_options}) + assertSuccess(result, test.expected, transform=_sorted_values, raw_res=True, msg=test.msg) + + +def test_wildcard_index_matches_collection_scan(collection): + """Wildcard index scan results match an unhinted (collection scan) query for a range.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"$**": 1}, "name": "wc_all"}]}, + ) + docs = [{"_id": i, "a": i * 3 % 7} for i in range(10)] + collection.insert_many(docs) + hinted = execute_command( + collection, + { + "find": collection.name, + "filter": {"a": {"$gte": 2}}, + "hint": "wc_all", + "sort": {"_id": 1}, + }, + ) + unhinted = execute_command( + collection, + {"find": collection.name, "filter": {"a": {"$gte": 2}}, "sort": {"_id": 1}}, + ) + assertSuccess( + hinted, + unhinted["cursor"]["firstBatch"], + msg="Wildcard IXSCAN results match collection scan results", + ) + + +def test_wildcard_hidden_created(collection): + """A wildcard index created with hidden:true is reported as hidden in listIndexes.""" + execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"$**": 1}, "name": "wc_hidden", "hidden": True}], + }, + ) + result = execute_command(collection, {"listIndexes": collection.name}) + + def _hidden(batch): + for idx in batch: + if idx["name"] == "wc_hidden": + return idx.get("hidden") + return None + + assertSuccess(result, True, transform=_hidden, msg="Hidden wildcard index reports hidden:true") diff --git a/documentdb_tests/framework/error_codes.py b/documentdb_tests/framework/error_codes.py index af7608d53..c3d05e297 100644 --- a/documentdb_tests/framework/error_codes.py +++ b/documentdb_tests/framework/error_codes.py @@ -400,6 +400,7 @@ MERGE_SPARSE_NULL_ON_FIELD_ERROR = 51132 MERGE_ON_ARRAY_ELEMENT_TYPE_ERROR = 51134 LET_SYSTEM_VARIABLE_IN_VALUE_ERROR = 51144 +CANNOT_USE_MIN_MAX_ERROR = 51174 MERGE_INTO_TYPE_ERROR = 51178 MERGE_UNSUPPORTED_MODE_COMBINATION_ERROR = 51181 MERGE_NO_MATCHING_UNIQUE_INDEX_ERROR = 51183 @@ -412,6 +413,7 @@ POSITIONAL_ARRAY_CONDITION_ERROR = 51246 POSITIONAL_INDEX_OUT_OF_BOUNDS_ERROR = 51247 PROJECT_EMPTY_SUB_PROJECTION_ERROR = 51270 +PROJECT_COMPUTED_FIELD_BANNED_ERROR = 51271 PROJECT_EMPTY_SPEC_ERROR = 51272 MERGE_LET_RESERVED_NEW_ERROR = 51273 NOT_SUPPORTED_ON_STANDALONE_ERROR = 51300 @@ -530,6 +532,8 @@ ENCRYPTED_FIELD_RANGE_MIN_MAX_TYPE_ERROR = 7018200 WILDCARD_MULTIPLE_FIELDS_ERROR = 7246201 WILDCARD_STRING_TYPE_ERROR = 7246202 +WILDCARD_COMPOUND_PREFIX_NOT_EXCLUDED_ERROR = 7246209 +WILDCARD_COMPOUND_PREFIX_MULTIKEY_ERROR = 7246301 OUT_TIMESERIES_COLLECTION_TYPE_ERROR = 7268700 MISSING_COMPACT_TOKEN_ERROR = 7294900 FCV_CONFIRM_REQUIRED_ERROR = 7369100