diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_basic_queries.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_basic_queries.py deleted file mode 100644 index 42266258a..000000000 --- a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_basic_queries.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -Basic find operation tests. - -Tests for fundamental find() and findOne() operations. -""" - -import pytest - -from documentdb_tests.framework.assertions import assertFailureCode, assertSuccess -from documentdb_tests.framework.executor import execute_command - - -@pytest.mark.find -@pytest.mark.smoke -def test_find_all_documents(collection): - """Test finding all documents in a collection.""" - collection.insert_many( - [ - {"_id": 0, "a": 1, "b": 1, "c": 1}, - {"_id": 1, "a": 1, "b": 2, "c": 1}, - {"_id": 2, "a": 2, "b": 1, "c": 1}, - ] - ) - result = execute_command(collection, {"find": collection.name}) - - expected = [ - {"_id": 0, "a": 1, "b": 1, "c": 1}, - {"_id": 1, "a": 1, "b": 2, "c": 1}, - {"_id": 2, "a": 2, "b": 1, "c": 1}, - ] - assertSuccess(result, expected, "Should return all 3 documents") - - -@pytest.mark.find -@pytest.mark.smoke -def test_find_with_filter(collection): - """Test find operation with a simple equality filter.""" - collection.insert_many( - [ - {"_id": 0, "a": 1, "b": 1, "c": 1}, - {"_id": 1, "a": 1, "b": 2, "c": 1}, - {"_id": 2, "a": 2, "b": 1, "c": 1}, - ] - ) - result = execute_command(collection, {"find": collection.name, "filter": {"b": 1}}) - - expected = [ - {"_id": 0, "a": 1, "b": 1, "c": 1}, - {"_id": 2, "a": 2, "b": 1, "c": 1}, - ] - assertSuccess(result, expected, "Should return only documents with b=1") - - -@pytest.mark.find -def test_find_one(collection): - """Test findOne operation returns a single document.""" - collection.insert_many( - [ - {"_id": 0, "a": 1, "b": 1, "c": 1}, - {"_id": 1, "a": 1, "b": 2, "c": 1}, - {"_id": 2, "a": 2, "b": 1, "c": 1}, - ] - ) - result = execute_command(collection, {"find": collection.name, "filter": {"b": 1}, "limit": 1}) - - expected = [{"_id": 0, "a": 1, "b": 1, "c": 1}] - assertSuccess(result, expected, "Should return single document with limit=1") - - -@pytest.mark.find -def test_find_one_not_found(collection): - """Test findOne returns None when no document matches.""" - collection.insert_many( - [ - {"_id": 0, "a": 1, "b": 1, "c": 1}, - {"_id": 1, "a": 1, "b": 2, "c": 1}, - ] - ) - result = execute_command(collection, {"find": collection.name, "filter": {"a": 2}, "limit": 1}) - - assertSuccess(result, [], "Should return empty array when no match") - - -@pytest.mark.find -def test_find_empty_collection(collection): - """Test find on an empty collection returns empty result.""" - result = execute_command(collection, {"find": collection.name}) - - assertSuccess(result, [], "Should return empty array for empty collection") - - -@pytest.mark.find -def test_find_with_multiple_conditions(collection): - """Test find with multiple filter conditions (implicit AND).""" - collection.insert_many( - [ - {"_id": 0, "a": 1, "b": 1, "c": 1}, - {"_id": 1, "a": 1, "b": 2, "c": 1}, - {"_id": 2, "a": 2, "b": 1, "c": 1}, - ] - ) - result = execute_command(collection, {"find": collection.name, "filter": {"a": 1, "b": 2}}) - - expected = [{"_id": 1, "a": 1, "b": 2, "c": 1}] - assertSuccess(result, expected, "Should return document matching both conditions") - - -@pytest.mark.find -def test_find_nested_field(collection): - """Test find with nested field query using dot notation.""" - collection.insert_many( - [ - {"_id": 0, "a": 1, "b": {"b1": 1}, "c": 1}, - {"_id": 1, "a": 1, "b": {"b1": 2}, "c": 1}, - {"_id": 2, "a": 2, "b": {"b1": 1}, "c": 1}, - ] - ) - result = execute_command(collection, {"find": collection.name, "filter": {"b.b1": 1}}) - - expected = [ - {"_id": 0, "a": 1, "b": {"b1": 1}, "c": 1}, - {"_id": 2, "a": 2, "b": {"b1": 1}, "c": 1}, - ] - assertSuccess(result, expected, "Should match nested field using dot notation") - - -@pytest.mark.find -def test_find_invalid_collection(collection): - """Test find on non-existent collection with invalid name.""" - result = execute_command(collection, {"find": "$invalid"}) - - assertFailureCode(result, 73, "Should reject invalid collection name") - - -@pytest.mark.find -def test_find_invalid_filter_type(collection): - """Test find with non-object filter returns error.""" - result = execute_command(collection, {"find": collection.name, "filter": "invalid"}) - - assertFailureCode(result, 14, "Should reject non-object filter") diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_combined.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_combined.py new file mode 100644 index 000000000..42925f14a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_combined.py @@ -0,0 +1,66 @@ +"""Tests for find command with multiple options combined.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Combined Operations]: find correctly applies filter, sort, projection, +# skip, limit, and let/$expr together in a single command. +FIND_COMBINED_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "filter_sort_projection_skip_limit", + docs=[{"_id": i, "a": i % 3, "b": i * 10, "c": f"val_{i}"} for i in range(10)], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"a": 0}, + "sort": {"b": -1}, + "projection": {"b": 1}, + "skip": 1, + "limit": 2, + }, + expected=[{"_id": 6, "b": 60}, {"_id": 3, "b": 30}], + msg="find should combine filter, sort, projection, skip, and limit correctly.", + ), + CommandTestCase( + "let_expr_sort_projection", + docs=[ + {"_id": 1, "score": 80, "name": "Alice"}, + {"_id": 2, "score": 95, "name": "Bob"}, + {"_id": 3, "score": 70, "name": "Charlie"}, + ], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"$expr": {"$gte": ["$score", "$$threshold"]}}, + "let": {"threshold": 80}, + "sort": {"score": -1}, + "projection": {"name": 1, "score": 1}, + }, + expected=[ + {"_id": 2, "name": "Bob", "score": 95}, + {"_id": 1, "name": "Alice", "score": 80}, + ], + msg="find should combine let, $expr, sort, and projection.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_COMBINED_TESTS)) +def test_find_combined(database_client, collection, test): + """Test find command combined operations.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_core_behavior.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_core_behavior.py new file mode 100644 index 000000000..39955c9e1 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_core_behavior.py @@ -0,0 +1,111 @@ +"""Tests for find command core behavior and response structure.""" + +from __future__ import annotations + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertProperties, assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq, Exists, IsType + +# Property [Primary Operation]: find returns matching documents from a collection, +# or all documents when no filter is specified. +FIND_CORE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "all_documents_no_filter", + docs=[{"_id": 1, "a": 10}, {"_id": 2, "a": 20}, {"_id": 3, "a": 30}], + command=lambda ctx: {"find": ctx.collection, "sort": {"_id": 1}}, + expected=[{"_id": 1, "a": 10}, {"_id": 2, "a": 20}, {"_id": 3, "a": 30}], + msg="find should return all documents when no filter specified.", + ), + CommandTestCase( + "filter_returns_matching", + docs=[{"_id": 1, "a": 10}, {"_id": 2, "a": 20}, {"_id": 3, "a": 10}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": 10}, "sort": {"_id": 1}}, + expected=[{"_id": 1, "a": 10}, {"_id": 3, "a": 10}], + msg="find should return only documents matching the filter.", + ), + CommandTestCase( + "empty_collection", + docs=[], + command=lambda ctx: {"find": ctx.collection}, + expected=[], + msg="find should return empty result for empty collection.", + ), + CommandTestCase( + "nonexistent_collection", + docs=None, + command=lambda ctx: {"find": ctx.collection}, + expected=[], + msg="find should return empty result for non-existent collection.", + ), + CommandTestCase( + "empty_filter_returns_all", + docs=[{"_id": 1, "x": "a"}, {"_id": 2, "x": "b"}], + command=lambda ctx: {"find": ctx.collection, "filter": {}, "sort": {"_id": 1}}, + expected=[{"_id": 1, "x": "a"}, {"_id": 2, "x": "b"}], + msg="find should return all documents with empty filter.", + ), + CommandTestCase( + "multiple_conditions_implicit_and", + docs=[{"_id": 1, "a": 1, "b": 2}, {"_id": 2, "a": 1, "b": 3}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": 1, "b": 2}}, + expected=[{"_id": 1, "a": 1, "b": 2}], + msg="find should use implicit AND for multiple filter conditions.", + ), + CommandTestCase( + "nested_field_dot_notation", + docs=[{"_id": 1, "obj": {"x": 10}}, {"_id": 2, "obj": {"x": 20}}], + command=lambda ctx: {"find": ctx.collection, "filter": {"obj.x": 10}}, + expected=[{"_id": 1, "obj": {"x": 10}}], + msg="find should match nested fields using dot notation.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_CORE_TESTS)) +def test_find_core_behavior(database_client, collection, test): + """Test find command core behavior.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + ) + + +def test_find_response_structure(collection): + """Test find response contains cursor with firstBatch, id, and ns fields.""" + collection.insert_one({"_id": 1, "a": 1}) + result = execute_command(collection, {"find": collection.name}) + assertResult( + result, + expected={ + "cursor.firstBatch": Exists(), + "cursor.id": IsType("long"), + "cursor.ns": IsType("string"), + }, + raw_res=True, + msg="find should return cursor with firstBatch, id, and ns.", + ) + + +def test_find_cursor_id_zero_when_exhausted(collection): + """Test cursor id is 0 when all results fit in first batch.""" + collection.insert_many([{"_id": i} for i in range(3)]) + result = execute_command(collection, {"find": collection.name}) + assertProperties( + result, + {"cursor.id": Eq(Int64(0))}, + raw_res=True, + msg="find should return cursor id 0 when all results returned.", + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_cursor_batch.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_cursor_batch.py new file mode 100644 index 000000000..97f3af9d4 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_cursor_batch.py @@ -0,0 +1,160 @@ +"""Tests for find command cursor and batch size behavior.""" + +from __future__ import annotations + +import pytest +from bson import Int64 + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertProperties, assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq, Len, Ne + +# Property [Batch Size]: find batchSize controls the number of documents in firstBatch +# and interacts correctly with limit and singleBatch. +FIND_BATCH_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "batchsize_zero_empty_first_batch", + docs=[{"_id": i} for i in range(5)], + command=lambda ctx: {"find": ctx.collection, "batchSize": 0}, + expected=[], + msg="find should return empty firstBatch when batchSize=0.", + ), + CommandTestCase( + "batchsize_one", + docs=[{"_id": i} for i in range(5)], + command=lambda ctx: {"find": ctx.collection, "batchSize": 1, "sort": {"_id": 1}}, + expected=[{"_id": 0}], + msg="find should return 1 document when batchSize=1.", + ), + CommandTestCase( + "batchsize_exceeds_total", + docs=[{"_id": i} for i in range(3)], + command=lambda ctx: {"find": ctx.collection, "batchSize": 100, "sort": {"_id": 1}}, + expected=[{"_id": 0}, {"_id": 1}, {"_id": 2}], + msg="find should return all documents when batchSize exceeds total.", + ), + CommandTestCase( + "singlebatch_true_returns_batchsize_docs", + docs=[{"_id": i} for i in range(10)], + command=lambda ctx: { + "find": ctx.collection, + "singleBatch": True, + "batchSize": 3, + "sort": {"_id": 1}, + }, + expected=[{"_id": 0}, {"_id": 1}, {"_id": 2}], + msg="find should return batchSize docs with singleBatch=true.", + ), + CommandTestCase( + "batchsize_with_limit", + docs=[{"_id": i} for i in range(10)], + command=lambda ctx: { + "find": ctx.collection, + "batchSize": 2, + "limit": 5, + "sort": {"_id": 1}, + }, + expected=[{"_id": 0}, {"_id": 1}], + msg="find should return batchSize docs in first batch when limit > batchSize.", + ), + CommandTestCase( + "limit_less_than_batchsize", + docs=[{"_id": i} for i in range(10)], + command=lambda ctx: { + "find": ctx.collection, + "batchSize": 5, + "limit": 2, + "sort": {"_id": 1}, + }, + expected=[{"_id": 0}, {"_id": 1}], + msg="find should return limit docs when limit < batchSize.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_BATCH_TESTS)) +def test_find_batch(database_client, collection, test): + """Test find command batch size behavior.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + ) + + +def test_find_batchsize_zero_cursor_open(collection): + """Test find with batchSize=0 returns non-zero cursor id.""" + collection.insert_many([{"_id": i} for i in range(5)]) + result = execute_command(collection, {"find": collection.name, "batchSize": 0}) + assertProperties( + result, + {"cursor.id": Ne(Int64(0))}, + raw_res=True, + msg="find should return non-zero cursor id when batchSize=0 and docs exist.", + ) + + +def test_find_batchsize_exceeds_total_cursor_closed(collection): + """Test find with batchSize > total returns cursor id = 0.""" + collection.insert_many([{"_id": i} for i in range(3)]) + result = execute_command(collection, {"find": collection.name, "batchSize": 100}) + assertProperties( + result, + {"cursor.id": Eq(Int64(0))}, + raw_res=True, + msg="find should close cursor when all results fit in batchSize.", + ) + + +def test_find_singlebatch_true_closes_cursor(collection): + """Test find with singleBatch=true returns cursor id = 0.""" + collection.insert_many([{"_id": i} for i in range(10)]) + result = execute_command( + collection, {"find": collection.name, "singleBatch": True, "batchSize": 3} + ) + assertProperties( + result, + {"cursor.id": Eq(Int64(0))}, + raw_res=True, + msg="find should close cursor when singleBatch=true.", + ) + + +def test_find_getmore_returns_remaining(collection): + """Test getMore after find returns remaining documents.""" + collection.insert_many([{"_id": i} for i in range(5)]) + result = execute_command( + collection, {"find": collection.name, "batchSize": 2, "sort": {"_id": 1}} + ) + cursor_id = result["cursor"]["id"] + getmore_result = execute_command( + collection, + {"getMore": cursor_id, "collection": collection.name, "batchSize": 10}, + ) + assertProperties( + getmore_result, + {"cursor": {"nextBatch": Eq([{"_id": 2}, {"_id": 3}, {"_id": 4}])}}, + raw_res=True, + msg="getMore should return remaining documents.", + ) + + +def test_find_default_batchsize_cap(collection): + """Test find default batchSize caps firstBatch at 101 documents.""" + collection.insert_many([{"_id": i} for i in range(150)]) + result = execute_command(collection, {"find": collection.name, "sort": {"_id": 1}}) + assertProperties( + result, + {"cursor.firstBatch": Len(101)}, + raw_res=True, + msg="find should return at most 101 documents in default first batch.", + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_field_validation.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_field_validation.py new file mode 100644 index 000000000..553de0ac0 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_field_validation.py @@ -0,0 +1,270 @@ +"""Tests for find command field type validation.""" + +from dataclasses import dataclass +from typing import Any + +import pytest +from bson import Int64 + +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + BAD_VALUE_ERROR, + INVALID_NAMESPACE_ERROR, + TYPE_MISMATCH_ERROR, + UNRECOGNIZED_COMMAND_FIELD_ERROR, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class FindFieldValidationTest(BaseTestCase): + """Test case for find command field validation.""" + + command: Any = None + + +# Property [Command Field Rejection]: find rejects non-string types for the collection +# name field. Wire-protocol namespace validation (INVALID_NAMESPACE_ERROR for non-string +# types) is foundational behavior per TEST_COVERAGE.md §19. One representative case wires +# find to that behavior; the full type matrix belongs in the centralized namespace test +# site (currently TBD). +FIND_FIELD_TESTS: list[FindFieldValidationTest] = [ + FindFieldValidationTest( + "find_field_rejects_non_string", + command={"find": 1}, + error_code=INVALID_NAMESPACE_ERROR, + msg="find should reject non-string type for collection name field.", + ), + FindFieldValidationTest( + "find_field_rejects_empty_string", + command={"find": ""}, + error_code=INVALID_NAMESPACE_ERROR, + msg="find should reject empty string collection name.", + ), + FindFieldValidationTest( + "find_field_rejects_dollar_prefix", + command={"find": "$invalid"}, + error_code=INVALID_NAMESPACE_ERROR, + msg="find should reject dollar-prefixed collection name.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_FIELD_TESTS)) +def test_find_field_validation(collection, test): + """Test find command field validation.""" + result = execute_command(collection, test.command) + assertResult(result, error_code=test.error_code, msg=test.msg) + + +# Property [Filter Type Rejection]: find rejects non-document types for the filter field. +FILTER_TYPE_TESTS: list[FindFieldValidationTest] = [ + FindFieldValidationTest( + "filter_rejects_string", + command=None, + error_code=TYPE_MISMATCH_ERROR, + msg="find should reject string filter.", + ), + FindFieldValidationTest( + "filter_rejects_integer", + command=None, + error_code=TYPE_MISMATCH_ERROR, + msg="find should reject integer filter.", + ), + FindFieldValidationTest( + "filter_rejects_boolean", + command=None, + error_code=TYPE_MISMATCH_ERROR, + msg="find should reject boolean filter.", + ), + FindFieldValidationTest( + "filter_rejects_array", + command=None, + error_code=TYPE_MISMATCH_ERROR, + msg="find should reject array filter.", + ), + FindFieldValidationTest( + "filter_rejects_null", + command=None, + error_code=TYPE_MISMATCH_ERROR, + msg="find should reject null filter.", + ), +] + +_FILTER_DYNAMIC_COMMANDS = { + "filter_rejects_string": lambda name: {"find": name, "filter": "invalid"}, + "filter_rejects_integer": lambda name: {"find": name, "filter": 123}, + "filter_rejects_boolean": lambda name: {"find": name, "filter": True}, + "filter_rejects_array": lambda name: {"find": name, "filter": [{"a": 1}]}, + "filter_rejects_null": lambda name: {"find": name, "filter": None}, +} + + +@pytest.mark.parametrize("test", pytest_params(FILTER_TYPE_TESTS)) +def test_find_filter_type_validation(collection, test): + """Test find filter field type validation.""" + command = _FILTER_DYNAMIC_COMMANDS[test.id](collection.name) + result = execute_command(collection, command) + assertResult(result, error_code=test.error_code, msg=test.msg) + + +# Property [Skip Validation]: find rejects invalid skip values. +SKIP_VALIDATION_TESTS: list[FindFieldValidationTest] = [ + FindFieldValidationTest( + "skip_rejects_negative", + command=None, + error_code=BAD_VALUE_ERROR, + msg="find should reject negative skip value.", + ), + FindFieldValidationTest( + "skip_rejects_string", + command=None, + error_code=TYPE_MISMATCH_ERROR, + msg="find should reject string skip value.", + ), +] + +_SKIP_DYNAMIC_COMMANDS = { + "skip_rejects_negative": lambda name: {"find": name, "skip": -1}, + "skip_rejects_string": lambda name: {"find": name, "skip": "invalid"}, +} + + +@pytest.mark.parametrize("test", pytest_params(SKIP_VALIDATION_TESTS)) +def test_find_skip_validation(collection, test): + """Test find skip field validation.""" + command = _SKIP_DYNAMIC_COMMANDS[test.id](collection.name) + result = execute_command(collection, command) + assertResult(result, error_code=test.error_code, msg=test.msg) + + +# Property [Limit Validation]: find rejects invalid limit values. +LIMIT_VALIDATION_TESTS: list[FindFieldValidationTest] = [ + FindFieldValidationTest( + "limit_rejects_negative", + command=None, + error_code=BAD_VALUE_ERROR, + msg="find should reject negative limit value.", + ), + FindFieldValidationTest( + "limit_rejects_string", + command=None, + error_code=TYPE_MISMATCH_ERROR, + msg="find should reject string limit value.", + ), +] + +_LIMIT_DYNAMIC_COMMANDS = { + "limit_rejects_negative": lambda name: {"find": name, "limit": -1}, + "limit_rejects_string": lambda name: {"find": name, "limit": "invalid"}, +} + + +@pytest.mark.parametrize("test", pytest_params(LIMIT_VALIDATION_TESTS)) +def test_find_limit_validation(collection, test): + """Test find limit field validation.""" + command = _LIMIT_DYNAMIC_COMMANDS[test.id](collection.name) + result = execute_command(collection, command) + assertResult(result, error_code=test.error_code, msg=test.msg) + + +def test_find_skip_accepts_whole_double(collection): + """Test find accepts whole-number double for skip.""" + collection.insert_many([{"_id": i} for i in range(5)]) + result = execute_command(collection, {"find": collection.name, "skip": 2.0, "sort": {"_id": 1}}) + assertResult( + result, + expected=[{"_id": 2}, {"_id": 3}, {"_id": 4}], + msg="find should accept whole-number double for skip.", + ) + + +def test_find_limit_accepts_whole_double(collection): + """Test find accepts whole-number double for limit.""" + collection.insert_many([{"_id": i} for i in range(5)]) + result = execute_command( + collection, {"find": collection.name, "limit": 2.0, "sort": {"_id": 1}} + ) + assertResult( + result, + expected=[{"_id": 0}, {"_id": 1}], + msg="find should accept whole-number double for limit.", + ) + + +def test_find_skip_accepts_int64(collection): + """Test find accepts Int64 for skip.""" + collection.insert_many([{"_id": i} for i in range(5)]) + result = execute_command( + collection, {"find": collection.name, "skip": Int64(2), "sort": {"_id": 1}} + ) + assertResult( + result, + expected=[{"_id": 2}, {"_id": 3}, {"_id": 4}], + msg="find should accept Int64 for skip.", + ) + + +def test_find_limit_accepts_int64(collection): + """Test find accepts Int64 for limit.""" + collection.insert_many([{"_id": i} for i in range(5)]) + result = execute_command( + collection, {"find": collection.name, "limit": Int64(2), "sort": {"_id": 1}} + ) + assertResult( + result, + expected=[{"_id": 0}, {"_id": 1}], + msg="find should accept Int64 for limit.", + ) + + +def test_find_sort_rejects_array(collection): + """Test find rejects array type for sort field.""" + result = execute_command(collection, {"find": collection.name, "sort": [1, -1]}) + assertResult( + result, + error_code=TYPE_MISMATCH_ERROR, + msg="find should reject array type for sort field.", + ) + + +def test_find_projection_rejects_string(collection): + """Test find rejects string type for projection field.""" + result = execute_command(collection, {"find": collection.name, "projection": "a"}) + assertResult( + result, + error_code=TYPE_MISMATCH_ERROR, + msg="find should reject string type for projection field.", + ) + + +def test_find_projection_rejects_integer(collection): + """Test find rejects integer type for projection field.""" + result = execute_command(collection, {"find": collection.name, "projection": 123}) + assertResult( + result, + error_code=TYPE_MISMATCH_ERROR, + msg="find should reject integer type for projection field.", + ) + + +def test_find_projection_rejects_array(collection): + """Test find rejects array type for projection field.""" + result = execute_command(collection, {"find": collection.name, "projection": ["a"]}) + assertResult( + result, + error_code=TYPE_MISMATCH_ERROR, + msg="find should reject array type for projection field.", + ) + + +def test_find_rejects_unrecognized_field(collection): + """Test find rejects unrecognized top-level command fields.""" + result = execute_command(collection, {"find": collection.name, "unknownField": 123}) + assertResult( + result, + error_code=UNRECOGNIZED_COMMAND_FIELD_ERROR, + msg="find should reject unrecognized command fields.", + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_filter_bson_types.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_filter_bson_types.py new file mode 100644 index 000000000..647fcbdb9 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_filter_bson_types.py @@ -0,0 +1,321 @@ +"""Tests for find filter matching across BSON types and numeric equivalence.""" + +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Code, Decimal128, Int64, MaxKey, MinKey, ObjectId, Timestamp + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult, assertSuccessNaN +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +_OID = ObjectId() +_DT = datetime(2024, 1, 1, tzinfo=timezone.utc) +_TS = Timestamp(1000, 1) +_CODE = Code("function(){}") + + +# Property [BSON Type Matching]: find filter matches documents by exact BSON type value +# for all non-deprecated types. +FIND_BSON_TYPE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "double", + docs=[{"_id": 1, "a": 3.14}, {"_id": 2, "a": 2.71}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": 3.14}}, + expected=[{"_id": 1, "a": 3.14}], + msg="find should match double.", + ), + CommandTestCase( + "int32", + docs=[{"_id": 1, "a": 42}, {"_id": 2, "a": 99}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": 42}}, + expected=[{"_id": 1, "a": 42}], + msg="find should match int32.", + ), + CommandTestCase( + "int64", + docs=[{"_id": 1, "a": Int64(9_000_000_000)}, {"_id": 2, "a": Int64(1)}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": Int64(9_000_000_000)}}, + expected=[{"_id": 1, "a": Int64(9_000_000_000)}], + msg="find should match Int64.", + ), + CommandTestCase( + "decimal128", + docs=[{"_id": 1, "a": Decimal128("1.5")}, {"_id": 2, "a": Decimal128("2.5")}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": Decimal128("1.5")}}, + expected=[{"_id": 1, "a": Decimal128("1.5")}], + msg="find should match Decimal128.", + ), + CommandTestCase( + "string", + docs=[{"_id": 1, "a": "hello"}, {"_id": 2, "a": "world"}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": "hello"}}, + expected=[{"_id": 1, "a": "hello"}], + msg="find should match string.", + ), + CommandTestCase( + "boolean_true", + docs=[{"_id": 1, "a": True}, {"_id": 2, "a": False}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": True}}, + expected=[{"_id": 1, "a": True}], + msg="find should match boolean true.", + ), + CommandTestCase( + "boolean_false", + docs=[{"_id": 1, "a": True}, {"_id": 2, "a": False}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": False}}, + expected=[{"_id": 2, "a": False}], + msg="find should match boolean false.", + ), + CommandTestCase( + "embedded_document", + docs=[{"_id": 1, "a": {"x": 1}}, {"_id": 2, "a": {"x": 2}}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": {"x": 1}}}, + expected=[{"_id": 1, "a": {"x": 1}}], + msg="find should match embedded doc.", + ), + CommandTestCase( + "array", + docs=[{"_id": 1, "a": [1, 2, 3]}, {"_id": 2, "a": [4, 5]}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": [1, 2, 3]}}, + expected=[{"_id": 1, "a": [1, 2, 3]}], + msg="find should match array exactly.", + ), + CommandTestCase( + "binary", + docs=[{"_id": 1, "a": Binary(b"data")}, {"_id": 2, "a": Binary(b"other")}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": Binary(b"data")}}, + expected=[{"_id": 1, "a": b"data"}], + msg="find should match Binary.", + ), + CommandTestCase( + "objectid", + docs=[{"_id": 1, "a": _OID}, {"_id": 2, "a": ObjectId()}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": _OID}}, + expected=[{"_id": 1, "a": _OID}], + msg="find should match ObjectId.", + ), + CommandTestCase( + "datetime", + docs=[{"_id": 1, "a": _DT}, {"_id": 2, "a": datetime(2025, 1, 1, tzinfo=timezone.utc)}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": _DT}}, + expected=[{"_id": 1, "a": _DT}], + msg="find should match datetime.", + ), + CommandTestCase( + "null", + docs=[{"_id": 1, "a": None}, {"_id": 2, "a": 1}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": None}}, + expected=[{"_id": 1, "a": None}], + msg="find should match null values.", + ), + CommandTestCase( + "timestamp", + docs=[{"_id": 1, "a": _TS}, {"_id": 2, "a": Timestamp(2000, 1)}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": _TS}}, + expected=[{"_id": 1, "a": _TS}], + msg="find should match Timestamp.", + ), + CommandTestCase( + "code", + docs=[{"_id": 1, "a": _CODE}, {"_id": 2, "a": Code("x")}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": _CODE}}, + expected=[{"_id": 1, "a": _CODE}], + msg="find should match Code.", + ), + CommandTestCase( + "minkey", + docs=[{"_id": 1, "a": MinKey()}, {"_id": 2, "a": 0}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": MinKey()}}, + expected=[{"_id": 1, "a": MinKey()}], + msg="find should match MinKey.", + ), + CommandTestCase( + "maxkey", + docs=[{"_id": 1, "a": MaxKey()}, {"_id": 2, "a": 0}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": MaxKey()}}, + expected=[{"_id": 1, "a": MaxKey()}], + msg="find should match MaxKey.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_BSON_TYPE_TESTS)) +def test_find_bson_type_matching(database_client, collection, test): + """Test find filter matches each BSON type.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + msg=test.msg, + ) + + +# Property [Null and Missing]: find filter {field: null} matches both null values +# and documents where the field is absent. +FIND_NULL_MISSING_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "null_matches_missing", + docs=[{"_id": 1, "a": None}, {"_id": 2, "a": 1}, {"_id": 3}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": None}, "sort": {"_id": 1}}, + expected=[{"_id": 1, "a": None}, {"_id": 3}], + msg="find should match both null and missing fields with {field: null}.", + ), + CommandTestCase( + "exists_true_excludes_missing", + docs=[{"_id": 1, "a": None}, {"_id": 2, "a": 1}, {"_id": 3}], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"a": {"$exists": True}}, + "sort": {"_id": 1}, + }, + expected=[{"_id": 1, "a": None}, {"_id": 2, "a": 1}], + msg="find should match null but not missing with $exists: true.", + ), + CommandTestCase( + "exists_false_matches_only_missing", + docs=[{"_id": 1, "a": None}, {"_id": 2, "a": 1}, {"_id": 3}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": {"$exists": False}}}, + expected=[{"_id": 3}], + msg="find should match only missing field with $exists: false.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_NULL_MISSING_TESTS)) +def test_find_null_missing(database_client, collection, test): + """Test find filter null and missing field behavior.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult(result, expected=test.build_expected(ctx), msg=test.msg) + + +# Property [Special Numeric Values]: find filter matches NaN and Infinity values +# across float and Decimal128 types. +FIND_SPECIAL_NUMERIC_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "float_infinity", + docs=[{"_id": 1, "a": float("inf")}, {"_id": 2, "a": 1.0}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": float("inf")}}, + expected=[{"_id": 1, "a": float("inf")}], + msg="find should match float Infinity.", + ), + CommandTestCase( + "float_negative_infinity", + docs=[{"_id": 1, "a": float("-inf")}, {"_id": 2, "a": 1.0}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": float("-inf")}}, + expected=[{"_id": 1, "a": float("-inf")}], + msg="find should match float -Infinity.", + ), + CommandTestCase( + "decimal128_nan", + docs=[{"_id": 1, "a": Decimal128("NaN")}, {"_id": 2, "a": Decimal128("1")}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": Decimal128("NaN")}}, + expected=[{"_id": 1, "a": Decimal128("NaN")}], + msg="find should match Decimal128 NaN.", + ), + CommandTestCase( + "decimal128_infinity", + docs=[{"_id": 1, "a": Decimal128("Infinity")}, {"_id": 2, "a": Decimal128("1")}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": Decimal128("Infinity")}}, + expected=[{"_id": 1, "a": Decimal128("Infinity")}], + msg="find should match Decimal128 Infinity.", + ), + CommandTestCase( + "decimal128_negative_infinity", + docs=[{"_id": 1, "a": Decimal128("-Infinity")}, {"_id": 2, "a": Decimal128("1")}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": Decimal128("-Infinity")}}, + expected=[{"_id": 1, "a": Decimal128("-Infinity")}], + msg="find should match Decimal128 -Infinity.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_SPECIAL_NUMERIC_TESTS)) +def test_find_special_numeric(database_client, collection, test): + """Test find filter special numeric value matching.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult(result, expected=test.build_expected(ctx), msg=test.msg) + + +def test_find_matches_float_nan(collection): + """Test filter matches float NaN value.""" + collection.insert_many([{"_id": 1, "a": float("nan")}, {"_id": 2, "a": 1.0}]) + result = execute_command(collection, {"find": collection.name, "filter": {"a": float("nan")}}) + assertSuccessNaN(result, [{"_id": 1, "a": float("nan")}], msg="find should match float NaN.") + + +# Property [Numeric Equivalence]: find filter treats numerically equivalent values +# across int32, int64, double, and Decimal128 as equal, but distinguishes bool from int. +FIND_NUMERIC_EQUIVALENCE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "equivalence_one", + docs=[ + {"_id": 1, "a": 1}, + {"_id": 2, "a": Int64(1)}, + {"_id": 3, "a": 1.0}, + {"_id": 4, "a": Decimal128("1")}, + {"_id": 5, "a": 2}, + ], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": 1}, "sort": {"_id": 1}}, + expected=[ + {"_id": 1, "a": 1}, + {"_id": 2, "a": Int64(1)}, + {"_id": 3, "a": 1.0}, + {"_id": 4, "a": Decimal128("1")}, + ], + msg="find should treat numerically equivalent values as equal.", + ), + CommandTestCase( + "equivalence_zero", + docs=[ + {"_id": 1, "a": 0}, + {"_id": 2, "a": Int64(0)}, + {"_id": 3, "a": 0.0}, + {"_id": 4, "a": Decimal128("0")}, + {"_id": 5, "a": 1}, + ], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": 0}, "sort": {"_id": 1}}, + expected=[ + {"_id": 1, "a": 0}, + {"_id": 2, "a": Int64(0)}, + {"_id": 3, "a": 0.0}, + {"_id": 4, "a": Decimal128("0")}, + ], + msg="find should treat numerically equivalent zero values as equal.", + ), + CommandTestCase( + "boolean_false_not_equal_zero", + docs=[{"_id": 1, "a": False}, {"_id": 2, "a": 0}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": False}}, + expected=[{"_id": 1, "a": False}], + msg="find should not match int 0 with boolean false.", + ), + CommandTestCase( + "boolean_true_not_equal_one", + docs=[{"_id": 1, "a": True}, {"_id": 2, "a": 1}], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": True}}, + expected=[{"_id": 1, "a": True}], + msg="find should not match int 1 with boolean true.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_NUMERIC_EQUIVALENCE_TESTS)) +def test_find_numeric_equivalence(database_client, collection, test): + """Test find filter numeric equivalence across BSON types.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult(result, expected=test.build_expected(ctx), msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_hint_min_max.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_hint_min_max.py new file mode 100644 index 000000000..353197bf0 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_hint_min_max.py @@ -0,0 +1,124 @@ +"""Tests for find command hint, min, and max fields.""" + +from __future__ import annotations + +import pytest +from pymongo import IndexModel + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Hint and Min/Max]: find accepts hint as index name or key pattern, +# and min/max define inclusive/exclusive bounds when paired with hint. +FIND_HINT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "hint_index_name", + docs=[{"_id": 1, "a": 10}, {"_id": 2, "a": 20}], + indexes=[IndexModel("a")], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": 10}, "hint": "a_1"}, + expected=[{"_id": 1, "a": 10}], + msg="find should accept hint with valid index name.", + ), + CommandTestCase( + "hint_key_pattern", + docs=[{"_id": 1, "a": 10}, {"_id": 2, "a": 20}], + indexes=[IndexModel("a")], + command=lambda ctx: {"find": ctx.collection, "filter": {"a": 10}, "hint": {"a": 1}}, + expected=[{"_id": 1, "a": 10}], + msg="find should accept hint with valid key pattern document.", + ), + CommandTestCase( + "hint_id_index", + docs=[{"_id": 1, "a": 10}, {"_id": 2, "a": 20}], + command=lambda ctx: {"find": ctx.collection, "filter": {"_id": 1}, "hint": "_id_"}, + expected=[{"_id": 1, "a": 10}], + msg="find should accept hint with _id_ index name.", + ), + CommandTestCase( + "hint_nonexistent_error", + docs=[{"_id": 1, "a": 10}], + command=lambda ctx: {"find": ctx.collection, "hint": "nonexistent_index"}, + error_code=BAD_VALUE_ERROR, + msg="find should reject hint for non-existent index.", + ), + CommandTestCase( + "min_with_hint", + docs=[{"_id": i, "a": i * 10} for i in range(1, 6)], + indexes=[IndexModel("a")], + command=lambda ctx: { + "find": ctx.collection, + "min": {"a": 30}, + "hint": {"a": 1}, + "sort": {"a": 1}, + }, + expected=[{"_id": 3, "a": 30}, {"_id": 4, "a": 40}, {"_id": 5, "a": 50}], + msg="find should return docs >= min bound with hint.", + ), + CommandTestCase( + "max_with_hint", + docs=[{"_id": i, "a": i * 10} for i in range(1, 6)], + indexes=[IndexModel("a")], + command=lambda ctx: { + "find": ctx.collection, + "max": {"a": 30}, + "hint": {"a": 1}, + "sort": {"a": 1}, + }, + expected=[{"_id": 1, "a": 10}, {"_id": 2, "a": 20}], + msg="find should return docs < max bound (exclusive) with hint.", + ), + CommandTestCase( + "min_max_range", + docs=[{"_id": i, "a": i * 10} for i in range(1, 6)], + indexes=[IndexModel("a")], + command=lambda ctx: { + "find": ctx.collection, + "min": {"a": 20}, + "max": {"a": 40}, + "hint": {"a": 1}, + "sort": {"a": 1}, + }, + expected=[{"_id": 2, "a": 20}, {"_id": 3, "a": 30}], + msg="find should return docs in [min, max) range with hint.", + ), + CommandTestCase( + "returnkey_true_with_index", + docs=[{"_id": 1, "a": 10, "b": 20}], + indexes=[IndexModel("a")], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"a": 10}, + "hint": "a_1", + "returnKey": True, + }, + expected=[{"a": 10}], + msg="find should return only index key fields with returnKey=true.", + ), + CommandTestCase( + "returnkey_true_no_index", + docs=[{"_id": 1, "a": 10}], + command=lambda ctx: {"find": ctx.collection, "returnKey": True}, + expected=[{}], + msg="find should return empty documents with returnKey=true and no index hint.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_HINT_TESTS)) +def test_find_hint(database_client, collection, test): + """Test find command hint, min, and max behavior.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_let_expr.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_let_expr.py new file mode 100644 index 000000000..6c8f88779 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_let_expr.py @@ -0,0 +1,115 @@ +"""Tests for find command $expr in filter and let variables.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [$expr and let]: find resolves $expr expressions and let variables +# in filter context, and let variables require $expr to be resolved. +FIND_LET_EXPR_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "expr_comparison", + docs=[{"_id": 1, "a": 5, "b": 3}, {"_id": 2, "a": 1, "b": 10}, {"_id": 3, "a": 7, "b": 7}], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"$expr": {"$gt": ["$a", "$b"]}}, + "sort": {"_id": 1}, + }, + expected=[{"_id": 1, "a": 5, "b": 3}], + msg="find should match docs where $a > $b via $expr.", + ), + CommandTestCase( + "expr_equality", + docs=[{"_id": 1, "a": 5}, {"_id": 2, "a": 10}], + command=lambda ctx: {"find": ctx.collection, "filter": {"$expr": {"$eq": ["$a", 5]}}}, + expected=[{"_id": 1, "a": 5}], + msg="find should match docs where $a equals literal via $expr.", + ), + CommandTestCase( + "expr_arithmetic", + docs=[{"_id": 1, "a": 8}, {"_id": 2, "a": 15}], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"$expr": {"$gt": [{"$add": ["$a", 1]}, 10]}}, + }, + expected=[{"_id": 2, "a": 15}], + msg="find should match via $expr with arithmetic.", + ), + CommandTestCase( + "let_variable_with_expr", + docs=[{"_id": 1, "a": 5}, {"_id": 2, "a": 10}], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"$expr": {"$eq": ["$a", "$$target"]}}, + "let": {"target": 5}, + }, + expected=[{"_id": 1, "a": 5}], + msg="find should resolve let variable in $expr filter.", + ), + CommandTestCase( + "let_multiple_variables", + docs=[{"_id": 1, "a": 5, "b": 10}, {"_id": 2, "a": 3, "b": 7}], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"$expr": {"$and": [{"$gte": ["$a", "$$lo"]}, {"$lte": ["$b", "$$hi"]}]}}, + "let": {"lo": 4, "hi": 10}, + }, + expected=[{"_id": 1, "a": 5, "b": 10}], + msg="find should resolve multiple let variables in $expr.", + ), + CommandTestCase( + "let_variable_string", + docs=[{"_id": 1, "name": "Alice"}, {"_id": 2, "name": "Bob"}], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"$expr": {"$eq": ["$name", "$$who"]}}, + "let": {"who": "Alice"}, + }, + expected=[{"_id": 1, "name": "Alice"}], + msg="find should resolve string let variable in $expr.", + ), + CommandTestCase( + "let_variable_null", + docs=[{"_id": 1, "a": None}, {"_id": 2, "a": 1}], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"$expr": {"$eq": ["$a", "$$val"]}}, + "let": {"val": None}, + }, + expected=[{"_id": 1, "a": None}], + msg="find should resolve null let variable in $expr.", + ), + CommandTestCase( + "let_not_resolved_without_expr", + docs=[{"_id": 1, "a": 5}, {"_id": 2, "a": "$$target"}], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"a": "$$target"}, + "let": {"target": 5}, + }, + expected=[{"_id": 2, "a": "$$target"}], + msg="find should not resolve let variable without $expr.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_LET_EXPR_TESTS)) +def test_find_let_expr(database_client, collection, test): + """Test find command $expr and let variable behavior.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_options.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_options.py new file mode 100644 index 000000000..faa9b0622 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_options.py @@ -0,0 +1,115 @@ +"""Tests for find command options: showRecordId, comment, noCursorTimeout, collation.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertProperties, assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Exists, IsType + +# Property [Option Acceptance]: find accepts cross-cutting options (comment, +# noCursorTimeout, allowDiskUse, collation, readConcern) without affecting results. +FIND_OPTION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "comment_string", + docs=[{"_id": 1, "a": 10}], + command=lambda ctx: {"find": ctx.collection, "comment": "test comment"}, + expected=[{"_id": 1, "a": 10}], + msg="find should accept string comment without affecting results.", + ), + CommandTestCase( + "comment_document", + docs=[{"_id": 1, "a": 10}], + command=lambda ctx: { + "find": ctx.collection, + "comment": {"purpose": "test", "version": 2}, + }, + expected=[{"_id": 1, "a": 10}], + msg="find should accept document comment without affecting results.", + ), + CommandTestCase( + "no_cursor_timeout_true", + docs=[{"_id": 1, "a": 10}], + command=lambda ctx: {"find": ctx.collection, "noCursorTimeout": True}, + expected=[{"_id": 1, "a": 10}], + msg="find should accept noCursorTimeout=true.", + ), + CommandTestCase( + "allow_disk_use_true", + docs=[{"_id": 1, "a": 10}], + command=lambda ctx: {"find": ctx.collection, "allowDiskUse": True}, + expected=[{"_id": 1, "a": 10}], + msg="find should accept allowDiskUse=true.", + ), + CommandTestCase( + "allow_disk_use_false", + docs=[{"_id": 1, "a": 10}], + command=lambda ctx: {"find": ctx.collection, "allowDiskUse": False}, + expected=[{"_id": 1, "a": 10}], + msg="find should accept allowDiskUse=false.", + ), + CommandTestCase( + "collation_acceptance", + docs=[{"_id": 1, "name": "Alice"}], + command=lambda ctx: { + "find": ctx.collection, + "collation": {"locale": "en", "strength": 2}, + }, + expected=[{"_id": 1, "name": "Alice"}], + msg="find should accept valid collation document.", + ), + CommandTestCase( + "read_concern_local", + docs=[{"_id": 1, "a": 10}], + command=lambda ctx: { + "find": ctx.collection, + "readConcern": {"level": "local"}, + }, + expected=[{"_id": 1, "a": 10}], + msg="find should accept readConcern level local.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_OPTION_TESTS)) +def test_find_options(database_client, collection, test): + """Test find command option acceptance.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + ) + + +def test_find_show_record_id(collection): + """Test find with showRecordId=true adds $recordId field.""" + collection.insert_many([{"_id": 1, "a": 10}]) + result = execute_command(collection, {"find": collection.name, "showRecordId": True}) + assertProperties( + result, + {"cursor.firstBatch.0.$recordId": Exists()}, + raw_res=True, + msg="find should add $recordId field when showRecordId=true.", + ) + + +def test_find_show_record_id_type(collection): + """Test find showRecordId returns Int64 value.""" + collection.insert_many([{"_id": 1, "a": 10}]) + result = execute_command(collection, {"find": collection.name, "showRecordId": True}) + assertResult( + result, + expected={"cursor.firstBatch.0.$recordId": IsType("long")}, + raw_res=True, + msg="find should return $recordId as Int64.", + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_projection.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_projection.py new file mode 100644 index 000000000..169cb545a --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_projection.py @@ -0,0 +1,148 @@ +"""Tests for find command projection functionality.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + PROJECT_EXCLUSION_IN_INCLUSION_ERROR, + PROJECT_PATH_COLLISION_CHILD_AFTER_PARENT_ERROR, + PROJECT_PATH_COLLISION_PARENT_AFTER_CHILD_ERROR, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Projection]: find inclusion/exclusion projections control which fields +# are returned, and invalid projections produce correct errors. +FIND_PROJECTION_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "inclusion", + docs=[{"_id": 1, "a": 10, "b": 20, "c": 30}], + command=lambda ctx: {"find": ctx.collection, "projection": {"a": 1, "b": 1}}, + expected=[{"_id": 1, "a": 10, "b": 20}], + msg="find should include only projected fields plus _id.", + ), + CommandTestCase( + "inclusion_exclude_id", + docs=[{"_id": 1, "a": 10, "b": 20}], + command=lambda ctx: {"find": ctx.collection, "projection": {"a": 1, "_id": 0}}, + expected=[{"a": 10}], + msg="find should exclude _id when _id: 0 with inclusion.", + ), + CommandTestCase( + "exclusion", + docs=[{"_id": 1, "a": 10, "b": 20, "c": 30}], + command=lambda ctx: {"find": ctx.collection, "projection": {"c": 0}}, + expected=[{"_id": 1, "a": 10, "b": 20}], + msg="find should exclude specified fields in exclusion projection.", + ), + CommandTestCase( + "mixed_inclusion_exclusion_error", + docs=[], + command=lambda ctx: {"find": ctx.collection, "projection": {"a": 1, "b": 0}}, + error_code=PROJECT_EXCLUSION_IN_INCLUSION_ERROR, + msg="find should reject mixed inclusion/exclusion projection.", + ), + CommandTestCase( + "nested_field_inclusion", + docs=[{"_id": 1, "obj": {"x": 10, "y": 20, "z": 30}}], + command=lambda ctx: {"find": ctx.collection, "projection": {"obj.x": 1}}, + expected=[{"_id": 1, "obj": {"x": 10}}], + msg="find should project nested field via dot notation.", + ), + CommandTestCase( + "nested_field_exclusion", + docs=[{"_id": 1, "obj": {"x": 10, "y": 20}}], + command=lambda ctx: {"find": ctx.collection, "projection": {"obj.y": 0}}, + expected=[{"_id": 1, "obj": {"x": 10}}], + msg="find should exclude nested field via dot notation.", + ), + CommandTestCase( + "path_collision_parent_then_child", + docs=[], + command=lambda ctx: {"find": ctx.collection, "projection": {"a": 1, "a.b": 1}}, + error_code=PROJECT_PATH_COLLISION_CHILD_AFTER_PARENT_ERROR, + msg="find should reject path collision (parent then child).", + ), + CommandTestCase( + "path_collision_child_then_parent", + docs=[], + command=lambda ctx: {"find": ctx.collection, "projection": {"a.b": 1, "a": 1}}, + error_code=PROJECT_PATH_COLLISION_PARENT_AFTER_CHILD_ERROR, + msg="find should reject path collision (child then parent).", + ), + CommandTestCase( + "slice_positive", + docs=[{"_id": 1, "arr": [10, 20, 30, 40, 50]}], + command=lambda ctx: {"find": ctx.collection, "projection": {"arr": {"$slice": 2}}}, + expected=[{"_id": 1, "arr": [10, 20]}], + msg="find should return first N elements with $slice positive.", + ), + CommandTestCase( + "slice_negative", + docs=[{"_id": 1, "arr": [10, 20, 30, 40, 50]}], + command=lambda ctx: {"find": ctx.collection, "projection": {"arr": {"$slice": -2}}}, + expected=[{"_id": 1, "arr": [40, 50]}], + msg="find should return last N elements with $slice negative.", + ), + CommandTestCase( + "slice_skip_limit", + docs=[{"_id": 1, "arr": [10, 20, 30, 40, 50]}], + command=lambda ctx: { + "find": ctx.collection, + "projection": {"arr": {"$slice": [1, 2]}}, + }, + expected=[{"_id": 1, "arr": [20, 30]}], + msg="find should apply [skip, limit] $slice form.", + ), + CommandTestCase( + "elemmatch", + docs=[{"_id": 1, "arr": [{"x": 1}, {"x": 2}, {"x": 3}]}], + command=lambda ctx: { + "find": ctx.collection, + "projection": {"arr": {"$elemMatch": {"x": {"$gte": 2}}}}, + }, + expected=[{"_id": 1, "arr": [{"x": 2}]}], + msg="find should return first matching array element with $elemMatch.", + ), + CommandTestCase( + "literal", + docs=[{"_id": 1, "a": 10}], + command=lambda ctx: { + "find": ctx.collection, + "projection": {"_id": 1, "val": {"$literal": 1}}, + }, + expected=[{"_id": 1, "val": 1}], + msg="find should set field to literal value with $literal.", + ), + CommandTestCase( + "filter_on_excluded_field", + docs=[{"_id": 1, "a": 10, "b": 20}, {"_id": 2, "a": 30, "b": 40}], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"b": 20}, + "projection": {"a": 1}, + }, + expected=[{"_id": 1, "a": 10}], + msg="find should filter on fields not in projection.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_PROJECTION_TESTS)) +def test_find_projection(database_client, collection, test): + """Test find command projection behavior.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_projections.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_projections.py deleted file mode 100644 index e3b543572..000000000 --- a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_projections.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -Projection tests for find operations. - -Tests for field inclusion, exclusion, and projection operators. -""" - -import pytest - -from documentdb_tests.framework.assertions import assertSuccess -from documentdb_tests.framework.executor import execute_command - - -@pytest.mark.find -def test_find_with_field_inclusion(collection): - """Test find with explicit field inclusion.""" - collection.insert_many( - [ - {"_id": 0, "a": "A", "b": 30, "c": "alice@example.com", "d": "NYC"}, - {"_id": 1, "a": "B", "b": 25, "c": "bob@example.com", "d": "SF"}, - ] - ) - result = execute_command(collection, {"find": collection.name, "projection": {"a": 1, "b": 1}}) - - expected = [{"_id": 0, "a": "A", "b": 30}, {"_id": 1, "a": "B", "b": 25}] - assertSuccess(result, expected, "Should include only specified fields") - - -@pytest.mark.find -def test_find_with_field_exclusion(collection): - """Test find with explicit field exclusion.""" - collection.insert_many( - [ - {"_id": 0, "a": "A", "b": 30, "c": "alice@example.com", "d": "NYC"}, - {"_id": 1, "a": "B", "b": 25, "c": "bob@example.com", "d": "SF"}, - ] - ) - result = execute_command(collection, {"find": collection.name, "projection": {"c": 0, "d": 0}}) - - expected = [{"_id": 0, "a": "A", "b": 30}, {"_id": 1, "a": "B", "b": 25}] - assertSuccess(result, expected, "Should exclude specified fields") - - -@pytest.mark.find -def test_find_exclude_id(collection): - """Test find with _id exclusion.""" - collection.insert_many( - [ - {"_id": 0, "a": "A", "b": 30, "c": "alice@example.com"}, - ] - ) - result = execute_command( - collection, {"find": collection.name, "projection": {"_id": 0, "a": 1, "b": 1}} - ) - - expected = [{"a": "A", "b": 30}] - assertSuccess(result, expected, "Should exclude _id field") - - -@pytest.mark.find -def test_find_nested_field_projection(collection): - """Test find with nested field projection.""" - collection.insert_many( - [ - {"_id": 0, "a": "A", "b": {"b1": 30, "b2": "NYC", "b3": "alice@example.com"}}, - ] - ) - result = execute_command( - collection, {"find": collection.name, "projection": {"a": 1, "b.b1": 1}} - ) - - expected = [{"_id": 0, "a": "A", "b": {"b1": 30}}] - assertSuccess(result, expected, "Should project nested field") - - -@pytest.mark.find -def test_find_empty_projection(collection): - """Test find with empty projection returns all fields.""" - collection.insert_many([{"_id": 0, "a": "A", "b": 30}]) - result = execute_command(collection, {"find": collection.name, "limit": 1}) - - expected = [{"_id": 0, "a": "A", "b": 30}] - assertSuccess(result, expected, "Should return all fields") diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_skip_limit.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_skip_limit.py new file mode 100644 index 000000000..6e3e6d2e4 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_skip_limit.py @@ -0,0 +1,111 @@ +"""Tests for find command skip and limit behavior.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +DOCS = [{"_id": i, "val": i * 10} for i in range(10)] + + +# Property [Skip and Limit]: find applies skip after sort and limit after skip, +# returning the correct pagination window. +FIND_SKIP_LIMIT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "skip_zero", + docs=DOCS, + command=lambda ctx: {"find": ctx.collection, "skip": 0, "sort": {"_id": 1}}, + expected=DOCS, + msg="find should return all documents when skip=0.", + ), + CommandTestCase( + "skip_n", + docs=DOCS, + command=lambda ctx: {"find": ctx.collection, "skip": 3, "sort": {"_id": 1}}, + expected=DOCS[3:], + msg="find should skip first N documents.", + ), + CommandTestCase( + "skip_exceeds_collection", + docs=DOCS, + command=lambda ctx: {"find": ctx.collection, "skip": 100, "sort": {"_id": 1}}, + expected=[], + msg="find should return empty when skip exceeds collection size.", + ), + CommandTestCase( + "limit_zero", + docs=DOCS, + command=lambda ctx: {"find": ctx.collection, "limit": 0, "sort": {"_id": 1}}, + expected=DOCS, + msg="find should return all documents when limit=0.", + ), + CommandTestCase( + "limit_one", + docs=DOCS, + command=lambda ctx: {"find": ctx.collection, "limit": 1, "sort": {"_id": 1}}, + expected=[DOCS[0]], + msg="find should return exactly 1 document when limit=1.", + ), + CommandTestCase( + "limit_n", + docs=DOCS, + command=lambda ctx: {"find": ctx.collection, "limit": 5, "sort": {"_id": 1}}, + expected=DOCS[:5], + msg="find should return at most N documents.", + ), + CommandTestCase( + "skip_and_limit_pagination", + docs=DOCS, + command=lambda ctx: {"find": ctx.collection, "skip": 3, "limit": 4, "sort": {"_id": 1}}, + expected=DOCS[3:7], + msg="find should apply skip then limit for pagination.", + ), + CommandTestCase( + "sort_skip_limit_order", + docs=[ + {"_id": 1, "a": 50}, + {"_id": 2, "a": 10}, + {"_id": 3, "a": 30}, + {"_id": 4, "a": 20}, + {"_id": 5, "a": 40}, + ], + command=lambda ctx: {"find": ctx.collection, "sort": {"a": 1}, "skip": 1, "limit": 2}, + expected=[{"_id": 4, "a": 20}, {"_id": 3, "a": 30}], + msg="find should apply sort before skip before limit.", + ), + CommandTestCase( + "skip_plus_limit_exceeds_count", + docs=DOCS[:5], + command=lambda ctx: {"find": ctx.collection, "skip": 3, "limit": 10, "sort": {"_id": 1}}, + expected=DOCS[3:5], + msg="find should return remaining docs when skip + limit exceeds total.", + ), + CommandTestCase( + "skip_equals_total", + docs=DOCS[:5], + command=lambda ctx: {"find": ctx.collection, "skip": 5, "limit": 5, "sort": {"_id": 1}}, + expected=[], + msg="find should return empty when skip equals collection size.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_SKIP_LIMIT_TESTS)) +def test_find_skip_limit(database_client, collection, test): + """Test find command skip and limit behavior.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_sort.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_sort.py new file mode 100644 index 000000000..fed2151ca --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_sort.py @@ -0,0 +1,127 @@ +"""Tests for find command sort functionality.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + SORT_ORDER_RANGE_ERROR, + SORT_ORDER_TYPE_ERROR, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +# Property [Sort Ordering]: find applies ascending, descending, and compound sort +# correctly, and rejects invalid sort specifications. +FIND_SORT_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "ascending", + docs=[{"_id": 1, "a": 30}, {"_id": 2, "a": 10}, {"_id": 3, "a": 20}], + command=lambda ctx: {"find": ctx.collection, "sort": {"a": 1}}, + expected=[{"_id": 2, "a": 10}, {"_id": 3, "a": 20}, {"_id": 1, "a": 30}], + msg="find should sort ascending by field value.", + ), + CommandTestCase( + "descending", + docs=[{"_id": 1, "a": 30}, {"_id": 2, "a": 10}, {"_id": 3, "a": 20}], + command=lambda ctx: {"find": ctx.collection, "sort": {"a": -1}}, + expected=[{"_id": 1, "a": 30}, {"_id": 3, "a": 20}, {"_id": 2, "a": 10}], + msg="find should sort descending by field value.", + ), + CommandTestCase( + "compound", + docs=[ + {"_id": 1, "a": 1, "b": 30}, + {"_id": 2, "a": 1, "b": 10}, + {"_id": 3, "a": 2, "b": 20}, + ], + command=lambda ctx: {"find": ctx.collection, "sort": {"a": 1, "b": -1}}, + expected=[ + {"_id": 1, "a": 1, "b": 30}, + {"_id": 2, "a": 1, "b": 10}, + {"_id": 3, "a": 2, "b": 20}, + ], + msg="find should apply compound sort with tiebreaker.", + ), + CommandTestCase( + "nested_field", + docs=[ + {"_id": 1, "obj": {"x": 30}}, + {"_id": 2, "obj": {"x": 10}}, + {"_id": 3, "obj": {"x": 20}}, + ], + command=lambda ctx: {"find": ctx.collection, "sort": {"obj.x": 1}}, + expected=[ + {"_id": 2, "obj": {"x": 10}}, + {"_id": 3, "obj": {"x": 20}}, + {"_id": 1, "obj": {"x": 30}}, + ], + msg="find should sort on nested field via dot notation.", + ), + CommandTestCase( + "value_zero_error", + docs=[], + command=lambda ctx: {"find": ctx.collection, "sort": {"a": 0}}, + error_code=SORT_ORDER_RANGE_ERROR, + msg="find should reject sort value 0.", + ), + CommandTestCase( + "value_two_error", + docs=[], + command=lambda ctx: {"find": ctx.collection, "sort": {"a": 2}}, + error_code=SORT_ORDER_RANGE_ERROR, + msg="find should reject sort value 2.", + ), + CommandTestCase( + "string_value_error", + docs=[], + command=lambda ctx: {"find": ctx.collection, "sort": {"a": "asc"}}, + error_code=SORT_ORDER_TYPE_ERROR, + msg="find should reject string sort order value.", + ), + CommandTestCase( + "empty_spec_accepted", + docs=[{"_id": 1, "a": 10}, {"_id": 2, "a": 20}], + command=lambda ctx: {"find": ctx.collection, "sort": {}}, + expected=[{"_id": 1, "a": 10}, {"_id": 2, "a": 20}], + msg="find should accept empty sort specification.", + ), + CommandTestCase( + "bson_type_ordering_wiring", + docs=[{"_id": 1, "a": "hello"}, {"_id": 2, "a": 42}, {"_id": 3, "a": None}], + command=lambda ctx: {"find": ctx.collection, "sort": {"a": 1}}, + expected=[ + {"_id": 3, "a": None}, + {"_id": 2, "a": 42}, + {"_id": 1, "a": "hello"}, + ], + msg="find should sort mixed types by BSON comparison order.", + ), + CommandTestCase( + "natural_order_insertion", + docs=[{"_id": 3, "x": "c"}, {"_id": 1, "x": "a"}, {"_id": 2, "x": "b"}], + command=lambda ctx: {"find": ctx.collection}, + expected=[{"_id": 3, "x": "c"}, {"_id": 1, "x": "a"}, {"_id": 2, "x": "b"}], + msg="find should return documents in insertion order when no sort specified.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_SORT_TESTS)) +def test_find_sort(database_client, collection, test): + """Test find command sort behavior.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + ignore_doc_order=test.id == "empty_spec_accepted", + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_tailable.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_tailable.py new file mode 100644 index 000000000..c1727698f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_tailable.py @@ -0,0 +1,64 @@ +"""Tests for find command tailable cursors on capped collections.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import BAD_VALUE_ERROR, FAILED_TO_PARSE_ERROR +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.target_collection import CappedCollection + +# Property [Tailable Cursors]: find supports tailable cursors on capped collections +# and rejects tailable/awaitData on non-capped collections. +FIND_TAILABLE_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "capped_collection", + target_collection=CappedCollection(size=10_000), + docs=[{"_id": i, "val": i * 10} for i in range(5)], + command=lambda ctx: {"find": ctx.collection, "sort": {"$natural": 1}}, + expected=[{"_id": i, "val": i * 10} for i in range(5)], + msg="find should return capped collection docs in insertion order.", + ), + CommandTestCase( + "tailable_on_capped", + target_collection=CappedCollection(size=10_000), + docs=[{"_id": i, "val": i * 10} for i in range(5)], + command=lambda ctx: {"find": ctx.collection, "tailable": True, "batchSize": 5}, + expected=[{"_id": i, "val": i * 10} for i in range(5)], + msg="find should accept tailable cursor on capped collection.", + ), + CommandTestCase( + "tailable_on_non_capped_error", + docs=[{"_id": 1}], + command=lambda ctx: {"find": ctx.collection, "tailable": True}, + error_code=BAD_VALUE_ERROR, + msg="find should reject tailable cursor on non-capped collection.", + ), + CommandTestCase( + "await_data_requires_tailable", + docs=[{"_id": 1}], + command=lambda ctx: {"find": ctx.collection, "awaitData": True}, + error_code=FAILED_TO_PARSE_ERROR, + msg="find should reject awaitData without tailable.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_TAILABLE_TESTS)) +def test_find_tailable(database_client, collection, test): + """Test find command tailable cursor behavior.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_views.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_views.py new file mode 100644 index 000000000..99c97533f --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_views.py @@ -0,0 +1,91 @@ +"""Tests for find command on views.""" + +from __future__ import annotations + +import pytest + +from documentdb_tests.compatibility.tests.core.utils.command_test_case import ( + CommandContext, + CommandTestCase, +) +from documentdb_tests.framework.assertions import assertResult +from documentdb_tests.framework.error_codes import ( + EXPRESSION_ARITY_ERROR, + UNRECOGNIZED_EXPRESSION_ERROR, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.target_collection import ViewCollection + +# Property [View Support]: find works on views and rejects unsupported projection +# operators that cannot be rewritten into the view pipeline. +FIND_VIEW_TESTS: list[CommandTestCase] = [ + CommandTestCase( + "view_returns_documents", + target_collection=ViewCollection(), + docs=[ + {"_id": 1, "a": 10, "b": "x"}, + {"_id": 2, "a": 20, "b": "y"}, + {"_id": 3, "a": 30, "b": "z"}, + ], + command=lambda ctx: { + "find": ctx.collection, + "filter": {"a": {"$gte": 20}}, + "sort": {"_id": 1}, + }, + expected=[{"_id": 2, "a": 20, "b": "y"}, {"_id": 3, "a": 30, "b": "z"}], + msg="find should return documents from view matching filter.", + ), + CommandTestCase( + "view_with_projection", + target_collection=ViewCollection(), + docs=[ + {"_id": 1, "a": 10, "b": "x"}, + {"_id": 2, "a": 20, "b": "y"}, + {"_id": 3, "a": 30, "b": "z"}, + ], + command=lambda ctx: { + "find": ctx.collection, + "projection": {"a": 1}, + "sort": {"_id": 1}, + }, + expected=[{"_id": 1, "a": 10}, {"_id": 2, "a": 20}, {"_id": 3, "a": 30}], + msg="find should support projection on views.", + ), + CommandTestCase( + "view_elemmatch_projection_error", + target_collection=ViewCollection(), + docs=[{"_id": 1, "a": [1, 2, 3]}], + command=lambda ctx: { + "find": ctx.collection, + "projection": {"a": {"$elemMatch": {"$gt": 1}}}, + }, + error_code=UNRECOGNIZED_EXPRESSION_ERROR, + msg="find should reject $elemMatch projection on views.", + ), + CommandTestCase( + "view_slice_projection_error", + target_collection=ViewCollection(), + docs=[{"_id": 1, "a": [1, 2, 3]}], + command=lambda ctx: { + "find": ctx.collection, + "projection": {"a": {"$slice": 1}}, + }, + error_code=EXPRESSION_ARITY_ERROR, + msg="find should reject $slice projection on views.", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_VIEW_TESTS)) +def test_find_views(database_client, collection, test): + """Test find command on views.""" + collection = test.prepare(database_client, collection) + ctx = CommandContext.from_collection(collection) + result = execute_command(collection, test.build_command(ctx)) + assertResult( + result, + expected=test.build_expected(ctx), + error_code=test.error_code, + msg=test.msg, + ) diff --git a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_with_expr.py b/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_with_expr.py deleted file mode 100644 index 34205e7b7..000000000 --- a/documentdb_tests/compatibility/tests/core/query_and_write/commands/find/test_find_with_expr.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Tests for $expr in find command contexts. -""" - -from documentdb_tests.framework.assertions import assertSuccess -from documentdb_tests.framework.executor import execute_command - -BASIC_DOCS = [ - {"_id": 1, "a": 5, "b": 3}, - {"_id": 2, "a": 1, "b": 10}, - {"_id": 3, "a": -1, "b": 0}, -] - - -def test_expr_let_in_find(collection): - """Test $expr with let variable in find command.""" - collection.insert_many(BASIC_DOCS) - result = execute_command( - collection, - { - "find": collection.name, - "filter": {"$expr": {"$eq": ["$a", "$$target"]}}, - "let": {"target": 5}, - }, - ) - assertSuccess(result, [{"_id": 1, "a": 5, "b": 3}]) - - -def test_expr_with_collation(collection): - """Test $expr with collation — string comparison respects collation rules.""" - collection.insert_many([{"_id": 1, "name": "apple"}, {"_id": 2, "name": "Banana"}]) - result = execute_command( - collection, - { - "find": collection.name, - "filter": {"$expr": {"$gt": ["$name", "banana"]}}, - "collation": {"locale": "en", "strength": 2}, - }, - ) - assertSuccess(result, [])