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..5bd3613b5 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_creation.py @@ -0,0 +1,469 @@ +"""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", + ), +] + + +@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..3cb45561d --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_data_types.py @@ -0,0 +1,414 @@ +"""Tests for wildcard index document structure, null/missing semantics, and write operations.""" + +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 + + +STRUCTURE_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="embedded_grandchild_scalar", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": {"b": {"c": 10}}}, {"_id": 2, "a": {"b": {"c": 20}}}), + filter={"a.b.c": 20}, + hint="wc_all", + expected=[{"_id": 2, "a": {"b": {"c": 20}}}], + msg="Grandchild scalar (two levels deep) matched via wildcard", + ), + 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_element_match", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": [1, 2, 3]}, {"_id": 2, "a": [4, 5]}), + filter={"a": 2}, + hint="wc_all", + expected=[{"_id": 1, "a": [1, 2, 3]}], + msg="Array element match via wildcard", + ), + IndexQueryTestCase( + id="array_of_objects_scalar", + indexes=({"key": {"$**": 1}, "name": "wc_all"},), + doc=({"_id": 1, "a": [{"b": 1}, {"b": 2}]}, {"_id": 2, "a": [{"b": 3}]}), + filter={"a.b": 2}, + hint="wc_all", + expected=[{"_id": 1, "a": [{"b": 1}, {"b": 2}]}], + msg="Scalar within array of objects", + ), + 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="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_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") 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..0b435c166 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_errors.py @@ -0,0 +1,349 @@ +"""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, + INVALID_INDEX_SPEC_OPTION_ERROR, + NO_QUERY_EXECUTION_PLANS_ERROR, + PROJECT_EXCLUSION_IN_INCLUSION_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", + ), +] + + +@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" + ) 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..b0488d59a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/wildcard/test_wildcard_query.py @@ -0,0 +1,669 @@ +"""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) + + +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) + + +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", + ), +] + + +@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..602169ad8 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