From 1ed0846c7978eef30ae96d28a2c061680b267a5f Mon Sep 17 00:00:00 2001 From: "Victor [C] Tsang" Date: Fri, 29 May 2026 20:48:44 +0000 Subject: [PATCH] Added index type tests for single Signed-off-by: Victor [C] Tsang --- .../core/indexes/types/single/__init__.py | 0 .../test_single_bson_type_validation.py | 73 +++++++ .../types/single/test_single_creation.py | 91 +++++++++ .../types/single/test_single_errors.py | 103 ++++++++++ .../types/single/test_single_properties.py | 47 +++++ .../types/single/test_single_queries.py | 192 ++++++++++++++++++ 6 files changed, 506 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/indexes/types/single/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_bson_type_validation.py create mode 100644 documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_creation.py create mode 100644 documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_properties.py create mode 100644 documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_queries.py diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/single/__init__.py b/documentdb_tests/compatibility/tests/core/indexes/types/single/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_bson_type_validation.py b/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_bson_type_validation.py new file mode 100644 index 000000000..12d5ca3f5 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_bson_type_validation.py @@ -0,0 +1,73 @@ +"""Tests for single field index key sort order BSON type validation. + +Verifies that single field index key values (sort order specifiers) reject +invalid BSON types and accept valid numeric types. +""" + +import pytest +from bson import Decimal128, Int64 + +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 +from documentdb_tests.framework.executor import execute_command + +pytestmark = pytest.mark.index + +SINGLE_KEY_SORT_ORDER_PARAMS = [ + BsonTypeTestCase( + id="sort_order", + msg="single key sort order should reject non-numeric types", + keyword="sort_order", + valid_types=[BsonType.DOUBLE, BsonType.INT, BsonType.LONG, BsonType.DECIMAL], + default_error_code=CANNOT_CREATE_INDEX_ERROR, + valid_inputs={ + BsonType.DOUBLE: 1.0, + BsonType.INT: 1, + BsonType.LONG: Int64(1), + BsonType.DECIMAL: Decimal128("1"), + }, + ), +] + + +REJECTION_CASES = generate_bson_rejection_test_cases(SINGLE_KEY_SORT_ORDER_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", REJECTION_CASES) +def test_single_key_sort_order_rejected(collection, bson_type, sample_value, spec): + """Test single field index creation rejects invalid BSON types for key sort order.""" + result = execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"a": sample_value}, "name": "test_idx"}], + }, + ) + assertFailureCode(result, spec.expected_code(bson_type), msg=spec.msg) + + +ACCEPTANCE_CASES = generate_bson_acceptance_test_cases(SINGLE_KEY_SORT_ORDER_PARAMS) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", ACCEPTANCE_CASES) +def test_single_key_sort_order_accepted(collection, bson_type, sample_value, spec): + """Test single field index creation accepts valid BSON types for key sort order. + + Note: This is a type validation test, not a functional test. We only verify + the command does not error — we do not check listIndexes to confirm the index + was created with the correct option value. + """ + result = execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"a": sample_value}, "name": "test_idx"}], + }, + ) + assertNotError(result, msg=f"sort order should accept {bson_type.value}") diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_creation.py b/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_creation.py new file mode 100644 index 000000000..c0a1174c1 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_creation.py @@ -0,0 +1,91 @@ +"""Tests for single field index creation. + +Validates valid argument handling, idempotency, and duplicate prevention. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.indexes.commands.utils.index_test_case import ( + IndexTestCase, + index_created_response, +) +from documentdb_tests.framework.assertions import assertSuccessPartial +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.index + +CREATION_SUCCESS_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="creation_ascending", + indexes=({"key": {"a": 1}, "name": "a_1"},), + msg="Ascending order succeeds", + ), + IndexTestCase( + id="creation_descending", + indexes=({"key": {"a": -1}, "name": "a_neg1"},), + msg="Descending order succeeds", + ), + IndexTestCase( + id="creation_dot_notation", + indexes=({"key": {"a.b": 1}, "name": "a.b_1"},), + msg="Dot notation field succeeds", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(CREATION_SUCCESS_TESTS)) +def test_single_creation_success(collection, test): + """Test single field index creation with valid arguments.""" + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + assertSuccessPartial(result, index_created_response(), test.msg) + + +def test_single_creation_on_nonexistent_collection(collection): + """Test createIndexes on non-existent collection creates collection and index.""" + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"x": 1}, "name": "x_1"}]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0, "numIndexesBefore": 1, "numIndexesAfter": 2}, + msg="Should create collection and index", + ) + + +def test_single_creation_idempotent(collection): + """Test creating same index twice is idempotent.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"a": 1}, "name": "a_1"}]}, + ) + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"a": 1}, "name": "a_1"}]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0, "numIndexesBefore": 2, "numIndexesAfter": 2}, + msg="Duplicate index creation should be no-op", + ) + + +def test_single_creation_different_sort_creates_two(collection): + """Test creating index with same field but different sort order creates two indexes.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"a": 1}, "name": "a_1"}]}, + ) + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"a": -1}, "name": "a_neg1"}]}, + ) + assertSuccessPartial( + result, + {"ok": 1.0, "numIndexesBefore": 2, "numIndexesAfter": 3}, + msg="Different sort order should create separate index", + ) diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_errors.py b/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_errors.py new file mode 100644 index 000000000..9996d693c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_errors.py @@ -0,0 +1,103 @@ +"""Tests for single field index error cases. + +Validates invalid sort values, field name errors, and index conflicts. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.indexes.commands.utils.index_test_case import ( + IndexTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode +from documentdb_tests.framework.error_codes import ( + CANNOT_CREATE_INDEX_ERROR, + INDEX_KEY_SPECS_CONFLICT_ERROR, + INDEX_OPTIONS_CONFLICT_ERROR, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.index + +CREATION_ERROR_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="invalid_sort_zero", + indexes=({"key": {"a": 0}, "name": "a_0"},), + error_code=CANNOT_CREATE_INDEX_ERROR, + msg="Sort order 0 should fail", + ), + IndexTestCase( + id="invalid_dollar_prefix", + indexes=({"key": {"$field": 1}, "name": "dollar_1"},), + error_code=CANNOT_CREATE_INDEX_ERROR, + msg="$ prefix field should fail", + ), + IndexTestCase( + id="invalid_empty_field", + indexes=({"key": {"": 1}, "name": "empty_1"},), + error_code=CANNOT_CREATE_INDEX_ERROR, + msg="Empty field name should fail", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(CREATION_ERROR_TESTS)) +def test_single_creation_error(collection, test): + """Test single field index creation with invalid arguments.""" + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + assertFailureCode(result, test.error_code, test.msg) + + +CONFLICT_ERROR_TESTS: list[IndexTestCase] = [ + IndexTestCase( + id="conflict_same_field_different_name", + setup_indexes=[{"key": {"a": 1}, "name": "a_1"}], + indexes=({"key": {"a": 1}, "name": "a_different"},), + error_code=INDEX_OPTIONS_CONFLICT_ERROR, + msg="Same field/order with different name should fail", + ), + IndexTestCase( + id="conflict_same_name_different_field", + setup_indexes=[{"key": {"a": 1}, "name": "my_idx"}], + indexes=({"key": {"b": 1}, "name": "my_idx"},), + error_code=INDEX_KEY_SPECS_CONFLICT_ERROR, + msg="Same name with different field should fail", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(CONFLICT_ERROR_TESTS)) +def test_single_conflict_error(collection, test): + """Test single field index creation conflicts.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.setup_indexes)}, + ) + result = execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + assertFailureCode(result, test.error_code, test.msg) + + +def test_single_option_conflict_same_key_name(collection): + """Test recreating index with same key+name but different options fails.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"a": 1}, "name": "a_1"}]}, + ) + result = execute_command( + collection, + { + "createIndexes": collection.name, + "indexes": [{"key": {"a": 1}, "name": "a_1", "unique": True}], + }, + ) + assertFailureCode( + result, + INDEX_KEY_SPECS_CONFLICT_ERROR, + msg="Same key+name with different options should fail", + ) diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_properties.py b/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_properties.py new file mode 100644 index 000000000..d791b6094 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_properties.py @@ -0,0 +1,47 @@ +"""Tests for single field index properties. + +Validates NaN and Infinity handling on single field indexes. +""" + +import pytest + +from documentdb_tests.framework.assertions import ( + assertSuccess, + assertSuccessNaN, +) +from documentdb_tests.framework.executor import execute_command + +pytestmark = pytest.mark.index + + +def test_single_nan_indexed_and_queryable(collection): + """Test NaN in indexed field is queryable.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"a": 1}, "name": "a_1"}]}, + ) + collection.insert_one({"_id": 1, "a": float("nan")}) + result = execute_command(collection, {"find": collection.name, "filter": {"a": float("nan")}}) + assertSuccessNaN(result, [{"_id": 1, "a": float("nan")}], msg="Should find NaN document") + + +def test_single_infinity_indexed(collection): + """Test Infinity in indexed field is queryable.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"a": 1}, "name": "a_1"}]}, + ) + collection.insert_one({"_id": 1, "a": float("inf")}) + result = execute_command(collection, {"find": collection.name, "filter": {"a": float("inf")}}) + assertSuccess(result, [{"_id": 1, "a": float("inf")}], msg="Should find Infinity document") + + +def test_single_negative_infinity_indexed(collection): + """Test -Infinity in indexed field is queryable.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"a": 1}, "name": "a_1"}]}, + ) + collection.insert_one({"_id": 1, "a": float("-inf")}) + result = execute_command(collection, {"find": collection.name, "filter": {"a": float("-inf")}}) + assertSuccess(result, [{"_id": 1, "a": float("-inf")}], msg="Should find -Infinity document") diff --git a/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_queries.py b/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_queries.py new file mode 100644 index 000000000..03bc39180 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/indexes/types/single/test_single_queries.py @@ -0,0 +1,192 @@ +"""Tests for single field index query behavior. + +Validates equality queries on various field types, null/missing field indexing, +sort order, descending index queries, and covered queries. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.indexes.commands.utils.index_test_case import ( + IndexQueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +pytestmark = pytest.mark.index + + +FIELD_QUERY_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="field_top_level_query", + indexes=({"key": {"score": 1}, "name": "score_1"},), + doc=({"_id": 1, "score": 10}, {"_id": 2, "score": 20}, {"_id": 3, "score": 30}), + filter={"score": 20}, + expected=[{"_id": 2, "score": 20}], + msg="Should find document by indexed field", + ), + IndexQueryTestCase( + id="field_embedded_query", + indexes=({"key": {"address.city": 1}, "name": "address.city_1"},), + doc=( + {"_id": 1, "address": {"city": "NYC", "zip": "10001"}}, + {"_id": 2, "address": {"city": "LA", "zip": "90001"}}, + ), + filter={"address.city": "NYC"}, + expected=[{"_id": 1, "address": {"city": "NYC", "zip": "10001"}}], + msg="Should find by embedded field", + ), + IndexQueryTestCase( + id="field_embedded_document_query", + indexes=({"key": {"address": 1}, "name": "address_1"},), + doc=( + {"_id": 1, "address": {"city": "NYC", "zip": "10001"}}, + {"_id": 2, "address": {"city": "LA", "zip": "90001"}}, + ), + filter={"address": {"city": "NYC", "zip": "10001"}}, + expected=[{"_id": 1, "address": {"city": "NYC", "zip": "10001"}}], + msg="Should match entire embedded document", + ), + IndexQueryTestCase( + id="field_embedded_document_order_matters", + indexes=({"key": {"address": 1}, "name": "address_1"},), + doc=({"_id": 1, "address": {"city": "NYC", "zip": "10001"}},), + filter={"address": {"zip": "10001", "city": "NYC"}}, + expected=[], + msg="Different field order should not match embedded document", + ), + IndexQueryTestCase( + id="null_value_indexed", + indexes=({"key": {"a": 1}, "name": "a_1"},), + doc=({"_id": 1, "a": None}, {"_id": 2, "a": 5}), + filter={"a": None}, + expected=[{"_id": 1, "a": None}], + msg="Should find document with null value", + ), + IndexQueryTestCase( + id="missing_field_as_null", + indexes=({"key": {"a": 1}, "name": "a_1"},), + doc=({"_id": 1, "b": 10}, {"_id": 2, "a": 5}), + filter={"a": None}, + expected=[{"_id": 1, "b": 10}], + msg="Missing field treated as null in query", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIELD_QUERY_TESTS)) +def test_single_field_query(collection, test): + """Test single field index queries on various field 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}) + assertSuccess(result, test.expected, msg=test.msg) + + +SORT_ORDER_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="sort_ascending_index_ascending", + indexes=({"key": {"a": 1}, "name": "a_1"},), + doc=({"_id": 1, "a": 3}, {"_id": 2, "a": 1}, {"_id": 3, "a": 2}), + sort={"a": 1}, + expected=[{"_id": 2, "a": 1}, {"_id": 3, "a": 2}, {"_id": 1, "a": 3}], + msg="Ascending index supports ascending sort", + ), + IndexQueryTestCase( + id="sort_ascending_index_descending", + indexes=({"key": {"a": 1}, "name": "a_1"},), + doc=({"_id": 1, "a": 3}, {"_id": 2, "a": 1}, {"_id": 3, "a": 2}), + sort={"a": -1}, + expected=[{"_id": 1, "a": 3}, {"_id": 3, "a": 2}, {"_id": 2, "a": 1}], + msg="Ascending index supports descending sort via reverse scan", + ), + IndexQueryTestCase( + id="sort_filter_and_sort", + indexes=({"key": {"a": 1}, "name": "a_1"},), + doc=({"_id": 1, "a": 5}, {"_id": 2, "a": 3}, {"_id": 3, "a": 7}, {"_id": 4, "a": 1}), + filter={"a": {"$gt": 2}}, + sort={"a": 1}, + expected=[{"_id": 2, "a": 3}, {"_id": 1, "a": 5}, {"_id": 3, "a": 7}], + msg="Index supports both filter and sort on same field", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(SORT_ORDER_TESTS)) +def test_single_sort_order(collection, test): + """Test single field index sort order behavior.""" + collection.insert_many(list(test.doc)) + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + cmd = {"find": collection.name} + if test.filter: + cmd["filter"] = test.filter + cmd["sort"] = test.sort + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg) + + +DESCENDING_INDEX_TESTS: list[IndexQueryTestCase] = [ + IndexQueryTestCase( + id="descending_equality", + indexes=({"key": {"a": -1}, "name": "a_neg1"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 10}), + filter={"a": 5}, + expected=[{"_id": 2, "a": 5}], + msg="Descending index should support equality query", + ), + IndexQueryTestCase( + id="descending_range_gt", + indexes=({"key": {"a": -1}, "name": "a_neg1"},), + doc=({"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "a": 10}), + filter={"a": {"$gt": 3}}, + expected=[{"_id": 2, "a": 5}, {"_id": 3, "a": 10}], + msg="Descending index should support $gt range query", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(DESCENDING_INDEX_TESTS)) +def test_single_descending_index(collection, test): + """Test descending single field index query behavior.""" + execute_command( + collection, + {"createIndexes": collection.name, "indexes": list(test.indexes)}, + ) + collection.insert_many(list(test.doc)) + cmd = {"find": collection.name, "filter": test.filter} + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, msg=test.msg, ignore_doc_order=True) + + +def test_single_covered_query_indexed_field_only(collection): + """Test single field index covers query with only indexed field projected.""" + collection.insert_many([{"_id": 1, "a": 10}, {"_id": 2, "a": 20}]) + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"a": 1}, "name": "a_1"}]}, + ) + result = execute_command( + collection, + {"find": collection.name, "filter": {"a": 10}, "projection": {"_id": 0, "a": 1}}, + ) + assertSuccess(result, [{"a": 10}], msg="Should return only indexed field") + + +def test_single_non_covered_query_extra_field(collection): + """Test single field index does NOT cover query with non-indexed field projected.""" + collection.insert_many([{"_id": 1, "a": 10, "b": "x"}, {"_id": 2, "a": 20, "b": "y"}]) + execute_command( + collection, + {"createIndexes": collection.name, "indexes": [{"key": {"a": 1}, "name": "a_1"}]}, + ) + result = execute_command( + collection, + {"find": collection.name, "filter": {"a": 10}, "projection": {"_id": 0, "a": 1, "b": 1}}, + ) + assertSuccess(result, [{"a": 10, "b": "x"}], msg="Should return indexed and non-indexed fields")