diff --git a/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/__init__.py b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_argument_handling.py b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_argument_handling.py new file mode 100644 index 00000000..fce8ea45 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_argument_handling.py @@ -0,0 +1,190 @@ +""" +Tests for $exists argument truthiness. + +Tests non-obvious truthy/falsy behavior when non-boolean values are passed +as the $exists argument. Only covers cases where the result is surprising +or where compatible engines are likely to diverge. +""" + +import pytest +from bson import Decimal128 + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_constants import ( + DECIMAL128_INFINITY, + DECIMAL128_MANY_TRAILING_ZEROS, + DECIMAL128_NAN, + DECIMAL128_NEGATIVE_INFINITY, + DECIMAL128_NEGATIVE_ZERO, + DECIMAL128_TRAILING_ZERO, + DECIMAL128_ZERO, + DOUBLE_NEGATIVE_ZERO, + FLOAT_NAN, +) + +DOCS = [{"_id": 1, "a": 1}, {"_id": 2, "b": 2}] +EXISTS_TRUE = [{"_id": 1, "a": 1}] +EXISTS_FALSE = [{"_id": 2, "b": 2}] + +ALL_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="true_matches_docs_with_field", + filter={"a": {"$exists": True}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: true matches documents with the field", + ), + QueryTestCase( + id="false_matches_docs_without_field", + filter={"a": {"$exists": False}}, + doc=DOCS, + expected=EXISTS_FALSE, + msg="$exists: false matches documents without the field", + ), + QueryTestCase( + id="int_1_as_true", + filter={"a": {"$exists": 1}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: 1 behaves as true", + ), + QueryTestCase( + id="int_0_as_false", + filter={"a": {"$exists": 0}}, + doc=DOCS, + expected=EXISTS_FALSE, + msg="$exists: 0 behaves as false", + ), + QueryTestCase( + id="negative_int_as_true", + filter={"a": {"$exists": -1}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: -1 behaves as true (non-zero)", + ), + QueryTestCase( + id="negative_zero_double_as_false", + filter={"a": {"$exists": DOUBLE_NEGATIVE_ZERO}}, + doc=DOCS, + expected=EXISTS_FALSE, + msg="$exists: -0.0 treated as false (zero)", + ), + QueryTestCase( + id="decimal128_zero_as_false", + filter={"a": {"$exists": DECIMAL128_ZERO}}, + doc=DOCS, + expected=EXISTS_FALSE, + msg="$exists: Decimal128('0') behaves as false", + ), + QueryTestCase( + id="decimal128_negative_zero_as_false", + filter={"a": {"$exists": DECIMAL128_NEGATIVE_ZERO}}, + doc=DOCS, + expected=EXISTS_FALSE, + msg="$exists: Decimal128('-0') treated as false (zero)", + ), + QueryTestCase( + id="decimal128_infinity_as_true", + filter={"a": {"$exists": DECIMAL128_INFINITY}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: Decimal128('Infinity') behaves as true", + ), + QueryTestCase( + id="decimal128_negative_infinity_as_true", + filter={"a": {"$exists": DECIMAL128_NEGATIVE_INFINITY}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: Decimal128('-Infinity') behaves as true", + ), + QueryTestCase( + id="decimal128_trailing_zero_as_true", + filter={"a": {"$exists": DECIMAL128_TRAILING_ZERO}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: Decimal128('1.0') behaves as true (non-zero)", + ), + QueryTestCase( + id="decimal128_many_trailing_zeros_as_true", + filter={"a": {"$exists": DECIMAL128_MANY_TRAILING_ZEROS}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: Decimal128 with many trailing zeros behaves as true (non-zero)", + ), + QueryTestCase( + id="decimal128_zero_trailing_decimals_as_false", + filter={"a": {"$exists": Decimal128("0.00")}}, + doc=DOCS, + expected=EXISTS_FALSE, + msg="$exists: Decimal128('0.00') treated as false (zero representation)", + ), + QueryTestCase( + id="decimal128_zero_exponent_as_false", + filter={"a": {"$exists": Decimal128("0E+10")}}, + doc=DOCS, + expected=EXISTS_FALSE, + msg="$exists: Decimal128('0E+10') treated as false (zero representation)", + ), + QueryTestCase( + id="decimal128_nan_as_true", + filter={"a": {"$exists": DECIMAL128_NAN}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: Decimal128('NaN') behaves as true (truthy)", + ), + QueryTestCase( + id="float_nan_as_true", + filter={"a": {"$exists": FLOAT_NAN}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: float('nan') behaves as true (truthy)", + ), + QueryTestCase( + id="empty_string_as_true", + filter={"a": {"$exists": ""}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: '' behaves as true (empty string is truthy in BSON)", + ), + QueryTestCase( + id="string_false_as_true", + filter={"a": {"$exists": "false"}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: 'false' behaves as true (non-empty string is truthy)", + ), + QueryTestCase( + id="empty_array_as_true", + filter={"a": {"$exists": []}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: [] behaves as true (truthy)", + ), + QueryTestCase( + id="empty_object_as_true", + filter={"a": {"$exists": {}}}, + doc=DOCS, + expected=EXISTS_TRUE, + msg="$exists: {} behaves as true (truthy)", + ), + QueryTestCase( + id="null_as_false", + filter={"a": {"$exists": None}}, + doc=DOCS, + expected=EXISTS_FALSE, + msg="$exists: None behaves as false", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_exists_argument_handling(collection, test): + """Parametrized test for $exists argument truthiness.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_array_and_special_fields.py b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_array_and_special_fields.py new file mode 100644 index 00000000..f7458184 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_array_and_special_fields.py @@ -0,0 +1,104 @@ +""" +Tests for $exists array field behavior, special field names, and multiple fields. + +Covers $exists on array fields, $elemMatch with $exists, _id field behavior, +and multiple $exists conditions. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +MULTI_DOCS = [ + {"_id": 1, "a": 1, "b": 2}, + {"_id": 2, "a": 1}, + {"_id": 3, "b": 2}, + {"_id": 4}, +] + +ARRAY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="elemMatch_exists_true", + filter={"a": {"$elemMatch": {"y": {"$exists": True}}}}, + doc=[{"_id": 1, "a": [{"x": 1, "y": 2}, {"x": 3}]}], + expected=[{"_id": 1, "a": [{"x": 1, "y": 2}, {"x": 3}]}], + msg="$elemMatch with $exists: true matches element with field", + ), + QueryTestCase( + id="elemMatch_exists_false", + filter={"a": {"$elemMatch": {"y": {"$exists": False}}}}, + doc=[{"_id": 1, "a": [{"x": 1, "y": 2}, {"x": 3}]}], + expected=[{"_id": 1, "a": [{"x": 1, "y": 2}, {"x": 3}]}], + msg="$elemMatch with $exists: false matches element without field", + ), + QueryTestCase( + id="elemMatch_compound", + filter={"a": {"$elemMatch": {"y": {"$exists": True}, "x": {"$gt": 0}}}}, + doc=[{"_id": 1, "a": [{"x": 1, "y": 2}, {"x": 3}]}], + expected=[{"_id": 1, "a": [{"x": 1, "y": 2}, {"x": 3}]}], + msg="$elemMatch compound with $exists and $gt", + ), + QueryTestCase( + id="dot_notation_nested_fields", + filter={"a.x": {"$exists": True}}, + doc=[ + {"_id": 1, "a": [{"x": 1}, {"x": 2}, {"y": 3}]}, + {"_id": 2, "a": [{"y": 1}, {"y": 2}]}, + ], + expected=[{"_id": 1, "a": [{"x": 1}, {"x": 2}, {"y": 3}]}], + msg="Dot notation on array elements matches when any has field", + ), +] + +SPECIAL_FIELD_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="id_always_true", + filter={"_id": {"$exists": True}}, + doc=[{"_id": 1}, {"_id": 2}, {"_id": 3}], + expected=[{"_id": 1}, {"_id": 2}, {"_id": 3}], + msg="_id always exists — $exists: true matches all", + ), + QueryTestCase( + id="id_false_no_match", + filter={"_id": {"$exists": False}}, + doc=[{"_id": 1}, {"_id": 2}, {"_id": 3}], + expected=[], + msg="_id always exists — $exists: false matches none", + ), + QueryTestCase( + id="both_exist", + filter={"a": {"$exists": True}, "b": {"$exists": True}}, + doc=MULTI_DOCS, + expected=[{"_id": 1, "a": 1, "b": 2}], + msg="Both fields must exist", + ), + QueryTestCase( + id="a_exists_b_not", + filter={"a": {"$exists": True}, "b": {"$exists": False}}, + doc=MULTI_DOCS, + expected=[{"_id": 2, "a": 1}], + msg="a exists, b does not", + ), + QueryTestCase( + id="neither_exists", + filter={"a": {"$exists": False}, "b": {"$exists": False}}, + doc=MULTI_DOCS, + expected=[{"_id": 4}], + msg="Neither field exists", + ), +] + +ALL_TESTS = ARRAY_TESTS + SPECIAL_FIELD_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_exists_array_and_special_fields(collection, test): + """Parametrized test for $exists array behavior, special fields, and multiple conditions.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_bson_type_coverage.py b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_bson_type_coverage.py new file mode 100644 index 00000000..ec47a96c --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_bson_type_coverage.py @@ -0,0 +1,187 @@ +""" +Tests for $exists BSON type coverage. + +Verifies $exists: true matches all BSON types when field is present (int, long, +double, decimal128, string, bool, null, object, array, binData, objectId, regex, +timestamp, date, minKey, maxKey), and $exists: false core semantics (null field +still exists, missing field matches). +""" + +from datetime import datetime, timezone + +import pytest +from bson import Binary, Decimal128, Int64, MaxKey, MinKey, ObjectId, Regex, Timestamp +from bson.codec_options import CodecOptions + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +EXISTS_TRUE_BSON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="int", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": 1}], + expected=[{"_id": 1, "a": 1}], + msg="$exists: true matches int field", + ), + QueryTestCase( + id="long", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": Int64(1)}], + expected=[{"_id": 1, "a": Int64(1)}], + msg="$exists: true matches long field", + ), + QueryTestCase( + id="double", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": 1.5}], + expected=[{"_id": 1, "a": 1.5}], + msg="$exists: true matches double field", + ), + QueryTestCase( + id="decimal128", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": Decimal128("1.0")}], + expected=[{"_id": 1, "a": Decimal128("1.0")}], + msg="$exists: true matches decimal128 field", + ), + QueryTestCase( + id="string", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": "hello"}], + expected=[{"_id": 1, "a": "hello"}], + msg="$exists: true matches string field", + ), + QueryTestCase( + id="bool_false", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": False}], + expected=[{"_id": 1, "a": False}], + msg="$exists: true matches boolean false field", + ), + QueryTestCase( + id="null", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": None}], + expected=[{"_id": 1, "a": None}], + msg="$exists: true matches null field (field exists with null value)", + ), + QueryTestCase( + id="object", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": {"b": 1}}], + expected=[{"_id": 1, "a": {"b": 1}}], + msg="$exists: true matches object field", + ), + QueryTestCase( + id="empty_object", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": {}}], + expected=[{"_id": 1, "a": {}}], + msg="$exists: true matches empty object field", + ), + QueryTestCase( + id="array", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": [1, 2, 3]}], + expected=[{"_id": 1, "a": [1, 2, 3]}], + msg="$exists: true matches array field", + ), + QueryTestCase( + id="empty_array", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": []}], + expected=[{"_id": 1, "a": []}], + msg="$exists: true matches empty array field", + ), + QueryTestCase( + id="empty_string", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": ""}], + expected=[{"_id": 1, "a": ""}], + msg="$exists: true matches empty string field", + ), + QueryTestCase( + id="zero", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": 0}], + expected=[{"_id": 1, "a": 0}], + msg="$exists: true matches zero field", + ), + QueryTestCase( + id="bindata", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": Binary(b"data", 0)}], + expected=[{"_id": 1, "a": b"data"}], + msg="$exists: true matches binData field", + ), + QueryTestCase( + id="bindata_user_defined", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": Binary(b"data", 128)}], + expected=[{"_id": 1, "a": Binary(b"data", 128)}], + msg="$exists: true matches binData subtype 128 (user-defined)", + ), + QueryTestCase( + id="objectid", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": ObjectId("000000000000000000000001")}], + expected=[{"_id": 1, "a": ObjectId("000000000000000000000001")}], + msg="$exists: true matches objectId field", + ), + QueryTestCase( + id="regex", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": Regex("pattern", "i")}], + expected=[{"_id": 1, "a": Regex("pattern", "i")}], + msg="$exists: true matches regex field", + ), + QueryTestCase( + id="timestamp", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": Timestamp(1, 1)}], + expected=[{"_id": 1, "a": Timestamp(1, 1)}], + msg="$exists: true matches timestamp field", + ), + QueryTestCase( + id="date", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": datetime(2024, 1, 1, tzinfo=timezone.utc)}], + expected=[{"_id": 1, "a": datetime(2024, 1, 1, tzinfo=timezone.utc)}], + msg="$exists: true matches date field", + ), + QueryTestCase( + id="minkey", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": MinKey()}], + expected=[{"_id": 1, "a": MinKey()}], + msg="$exists: true matches minKey field", + ), + QueryTestCase( + id="maxkey", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "a": MaxKey()}], + expected=[{"_id": 1, "a": MaxKey()}], + msg="$exists: true matches maxKey field", + ), +] + +ALL_TESTS = EXISTS_TRUE_BSON_TESTS + + +TZ_AWARE_CODEC = CodecOptions(tz_aware=True) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_exists_field_value_types(collection, test): + """Parametrized test for $exists field value type handling.""" + collection.insert_many(test.doc) + codec = TZ_AWARE_CODEC + result = execute_command( + collection, {"find": collection.name, "filter": test.filter}, codec_options=codec + ) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_edge_cases.py b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_edge_cases.py new file mode 100644 index 00000000..93e5a7bb --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_edge_cases.py @@ -0,0 +1,124 @@ +""" +Tests for $exists edge cases and null/missing field distinction. + +Covers null vs missing field semantics, empty collection, all/no documents +having the field, and falsy values (zero, false, empty string, empty array, +empty object) do NOT match $exists: false. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +NULL_MISSING_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="true_mixed_presence", + filter={"a": {"$exists": True}}, + doc=[ + {"_id": 1, "a": None, "b": 1}, + {"_id": 2, "b": 2}, + {"_id": 3, "a": 1, "b": 3}, + {"_id": 4, "a": False, "b": 4}, + ], + expected=[ + {"_id": 1, "a": None, "b": 1}, + {"_id": 3, "a": 1, "b": 3}, + {"_id": 4, "a": False, "b": 4}, + ], + msg="$exists: true returns all docs with field a (including null and false)", + ), + QueryTestCase( + id="false_mixed_presence", + filter={"a": {"$exists": False}}, + doc=[ + {"_id": 1, "a": None, "b": 1}, + {"_id": 2, "b": 2}, + {"_id": 3, "a": 1, "b": 3}, + {"_id": 4, "a": False, "b": 4}, + ], + expected=[{"_id": 2, "b": 2}], + msg="$exists: false returns only doc without field a", + ), +] + +EDGE_CASE_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="empty_collection_true", + filter={"a": {"$exists": True}}, + doc=[], + expected=[], + msg="$exists: true on empty collection returns nothing", + ), + QueryTestCase( + id="empty_collection_false", + filter={"a": {"$exists": False}}, + doc=[], + expected=[], + msg="$exists: false on empty collection returns nothing", + ), + QueryTestCase( + id="all_have_field", + filter={"a": {"$exists": False}}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 2}], + expected=[], + msg="$exists: false returns nothing when all docs have field", + ), + QueryTestCase( + id="none_have_field", + filter={"a": {"$exists": True}}, + doc=[{"_id": 1, "b": 1}, {"_id": 2, "b": 2}], + expected=[], + msg="$exists: true returns nothing when no docs have field", + ), + QueryTestCase( + id="zero_not_false", + filter={"a": {"$exists": False}}, + doc=[{"_id": 1, "a": 0}], + expected=[], + msg="Zero value does NOT match $exists: false", + ), + QueryTestCase( + id="false_value_not_false", + filter={"a": {"$exists": False}}, + doc=[{"_id": 1, "a": False}], + expected=[], + msg="False value does NOT match $exists: false", + ), + QueryTestCase( + id="empty_string_not_false", + filter={"a": {"$exists": False}}, + doc=[{"_id": 1, "a": ""}], + expected=[], + msg="Empty string does NOT match $exists: false", + ), + QueryTestCase( + id="empty_array_not_false", + filter={"a": {"$exists": False}}, + doc=[{"_id": 1, "a": []}], + expected=[], + msg="Empty array does NOT match $exists: false", + ), + QueryTestCase( + id="empty_object_not_false", + filter={"a": {"$exists": False}}, + doc=[{"_id": 1, "a": {}}], + expected=[], + msg="Empty object does NOT match $exists: false", + ), +] + +ALL_TESTS = NULL_MISSING_TESTS + EDGE_CASE_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_exists_edge_cases(collection, test): + """Parametrized test for $exists edge cases and null/missing distinction.""" + if test.doc: + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_nested_field_paths.py b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_nested_field_paths.py new file mode 100644 index 00000000..abad5fde --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_nested_field_paths.py @@ -0,0 +1,257 @@ +""" +Tests for $exists with nested field paths (dot notation). + +Covers simple nested objects, deep paths, array traversal, array indices, +numeric keys on objects, path traversal through non-objects, and special characters. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +SIMPLE_NESTED_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="nested_exists", + filter={"a.b": {"$exists": True}}, + doc=[{"_id": 1, "a": {"b": 1}}], + expected=[{"_id": 1, "a": {"b": 1}}], + msg="Dot notation matches nested field", + ), + QueryTestCase( + id="nested_not_exists", + filter={"a.b": {"$exists": False}}, + doc=[{"_id": 1, "a": {"c": 1}}], + expected=[{"_id": 1, "a": {"c": 1}}], + msg="Dot notation matches when nested field absent", + ), + QueryTestCase( + id="nested_null_exists", + filter={"a.b": {"$exists": True}}, + doc=[{"_id": 1, "a": {"b": None}}], + expected=[{"_id": 1, "a": {"b": None}}], + msg="Dot notation matches nested null field (field exists)", + ), + QueryTestCase( + id="empty_object_parent_false", + filter={"a.b": {"$exists": False}}, + doc=[{"_id": 1, "a": {}}], + expected=[{"_id": 1, "a": {}}], + msg="Empty object parent — nested field does not exist", + ), + QueryTestCase( + id="empty_object_parent_true", + filter={"a.b": {"$exists": True}}, + doc=[{"_id": 1, "a": {}}], + expected=[], + msg="Empty object parent — nested field does not match", + ), +] + +DEEP_NESTED_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="deep_nested_exists", + filter={"a.b.c.d": {"$exists": True}}, + doc=[{"_id": 1, "a": {"b": {"c": {"d": 1}}}}], + expected=[{"_id": 1, "a": {"b": {"c": {"d": 1}}}}], + msg="Deep dot notation matches", + ), + QueryTestCase( + id="deep_nested_not_exists", + filter={"a.b.c.d": {"$exists": False}}, + doc=[{"_id": 1, "a": {"b": {"c": 1}}}], + expected=[{"_id": 1, "a": {"b": {"c": 1}}}], + msg="Deep dot notation matches when path incomplete", + ), +] + +ARRAY_PATH_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="array_dot_all_have_field", + filter={"a.b": {"$exists": True}}, + doc=[{"_id": 1, "a": [{"b": 1}, {"b": 2}]}], + expected=[{"_id": 1, "a": [{"b": 1}, {"b": 2}]}], + msg="Dot notation through array matches when elements have field", + ), + QueryTestCase( + id="array_dot_none_have_field", + filter={"a.b": {"$exists": False}}, + doc=[{"_id": 1, "a": [{"c": 1}, {"c": 2}]}], + expected=[{"_id": 1, "a": [{"c": 1}, {"c": 2}]}], + msg="Dot notation through array matches false when no element has field", + ), + QueryTestCase( + id="array_dot_some_have_field", + filter={"a.b": {"$exists": True}}, + doc=[{"_id": 1, "a": [{"b": 1}, {"c": 2}]}], + expected=[{"_id": 1, "a": [{"b": 1}, {"c": 2}]}], + msg="Dot notation through array matches when at least one element has field", + ), + QueryTestCase( + id="deeply_nested_array_paths", + filter={"a.b.c": {"$exists": True}}, + doc=[{"_id": 1, "a": [{"b": [{"c": 1}]}, {"b": [{"c": 2}]}]}], + expected=[{"_id": 1, "a": [{"b": [{"c": 1}]}, {"b": [{"c": 2}]}]}], + msg="Deeply nested array paths match", + ), + QueryTestCase( + id="mixed_array_elements", + filter={"a.b": {"$exists": True}}, + doc=[{"_id": 1, "a": [1, {"b": 2}, "str", None]}], + expected=[{"_id": 1, "a": [1, {"b": 2}, "str", None]}], + msg="Mixed array elements — matches when any element has field", + ), + QueryTestCase( + id="array_null_element_has_field", + filter={"a.b": {"$exists": True}}, + doc=[{"_id": 1, "a": [{"b": None}, {"c": 1}]}], + expected=[{"_id": 1, "a": [{"b": None}, {"c": 1}]}], + msg="Array element with null field still matches $exists: true", + ), + QueryTestCase( + id="doubly_nested_array_no_traverse", + filter={"a.b": {"$exists": False}}, + doc=[{"_id": 1, "a": [[{"b": 1}]]}], + expected=[{"_id": 1, "a": [[{"b": 1}]]}], + msg="$exists does not traverse into nested arrays within arrays", + ), + QueryTestCase( + id="doubly_nested_array_true_no_traverse", + filter={"a.b": {"$exists": True}}, + doc=[{"_id": 1, "a": [[{"b": 1}]]}], + expected=[], + msg="$exists true does not match through doubly nested arrays", + ), +] + +ARRAY_INDEX_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="index_0_exists", + filter={"a.0": {"$exists": True}}, + doc=[{"_id": 1, "a": [10, 20, 30]}], + expected=[{"_id": 1, "a": [10, 20, 30]}], + msg="Array index 0 exists", + ), + QueryTestCase( + id="index_out_of_bounds", + filter={"a.5": {"$exists": False}}, + doc=[{"_id": 1, "a": [10, 20, 30]}], + expected=[{"_id": 1, "a": [10, 20, 30]}], + msg="Array index out of bounds matches $exists: false", + ), + QueryTestCase( + id="empty_array_index_0", + filter={"a.0": {"$exists": False}}, + doc=[{"_id": 1, "a": []}], + expected=[{"_id": 1, "a": []}], + msg="Empty array index 0 matches $exists: false", + ), + QueryTestCase( + id="very_large_index", + filter={"a.999999": {"$exists": False}}, + doc=[{"_id": 1, "a": [1, 2, 3]}], + expected=[{"_id": 1, "a": [1, 2, 3]}], + msg="Very large array index matches $exists: false", + ), +] + +NUMERIC_KEY_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="object_numeric_key", + filter={"a.0": {"$exists": True}}, + doc=[{"_id": 1, "a": {"0": "value"}}], + expected=[{"_id": 1, "a": {"0": "value"}}], + msg="Numeric key on object matches", + ), +] + +SPECIAL_PATH_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="negative_array_index", + filter={"a.-1": {"$exists": False}}, + doc=[{"_id": 1, "a": [1, 2, 3]}], + expected=[{"_id": 1, "a": [1, 2, 3]}], + msg="Negative array index matches $exists: false", + ), + QueryTestCase( + id="double_dot_path", + filter={"a..b": {"$exists": True}}, + doc=[{"_id": 1, "a": {"b": 1}}], + expected=[], + msg="Double dot in path does not match", + ), + QueryTestCase( + id="leading_dot_path", + filter={".a": {"$exists": True}}, + doc=[{"_id": 1, "a": 1}], + expected=[], + msg="Leading dot in path does not match", + ), + QueryTestCase( + id="trailing_dot_path", + filter={"a.": {"$exists": True}}, + doc=[{"_id": 1, "a": 1}], + expected=[], + msg="Trailing dot in path does not match", + ), + QueryTestCase( + id="deeply_nested_array_false", + filter={"a.b.c": {"$exists": False}}, + doc=[{"_id": 1, "a": [{"b": [{"d": 1}]}]}], + expected=[{"_id": 1, "a": [{"b": [{"d": 1}]}]}], + msg="Deeply nested array path matches $exists: false when field absent", + ), +] + +NON_OBJECT_TRAVERSAL_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="scalar_traversal_not_match", + filter={"a.b": {"$exists": True}}, + doc=[{"_id": 1, "a": 5}], + expected=[], + msg="Cannot traverse scalar — $exists: true does not match", + ), + QueryTestCase( + id="scalar_traversal_false_match", + filter={"a.b": {"$exists": False}}, + doc=[{"_id": 1, "a": 5}], + expected=[{"_id": 1, "a": 5}], + msg="Cannot traverse scalar — $exists: false matches", + ), + QueryTestCase( + id="null_traversal_false_match", + filter={"a.b": {"$exists": False}}, + doc=[{"_id": 1, "a": None}], + expected=[{"_id": 1, "a": None}], + msg="Cannot traverse null — $exists: false matches", + ), + QueryTestCase( + id="entire_path_missing", + filter={"a.b.c": {"$exists": False}}, + doc=[{"_id": 1, "x": 1}], + expected=[{"_id": 1, "x": 1}], + msg="Entire path missing matches $exists: false", + ), +] + +ALL_TESTS = ( + SIMPLE_NESTED_TESTS + + DEEP_NESTED_TESTS + + ARRAY_PATH_TESTS + + ARRAY_INDEX_TESTS + + NUMERIC_KEY_TESTS + + SPECIAL_PATH_TESTS + + NON_OBJECT_TRAVERSAL_TESTS +) + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_exists_nested_field_paths(collection, test): + """Parametrized test for $exists with nested field paths.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_operator_combinations.py b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_operator_combinations.py new file mode 100644 index 00000000..efcff6e7 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_operator_combinations.py @@ -0,0 +1,162 @@ +""" +Tests for $exists combined with other query operators. + +Covers $exists with comparison, $type, $in, $nin, $regex, $size, $all, +$not, and logical operators. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +DOCS: list[dict] = [ + {"_id": 1, "a": 1}, + {"_id": 2, "a": None}, + {"_id": 3, "b": 1}, +] + +COMPARISON_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="exists_and_gt", + filter={"a": {"$exists": True, "$gt": 0}}, + doc=[{"_id": 1, "a": 10}, {"_id": 2, "a": -1}, {"_id": 3, "b": 1}], + expected=[{"_id": 1, "a": 10}], + msg="$exists: true AND $gt: 0", + ), + QueryTestCase( + id="exists_and_eq_null", + filter={"a": {"$exists": True, "$eq": None}}, + doc=DOCS, + expected=[{"_id": 2, "a": None}], + msg="$exists: true AND $eq: null — field exists and is null", + ), + QueryTestCase( + id="exists_and_ne_null", + filter={"a": {"$exists": True, "$ne": None}}, + doc=DOCS, + expected=[{"_id": 1, "a": 1}], + msg="$exists: true AND $ne: null — field exists and is not null", + ), + QueryTestCase( + id="exists_and_type_string", + filter={"a": {"$exists": True, "$type": "string"}}, + doc=[{"_id": 1, "a": "hello"}, {"_id": 2, "a": 123}, {"_id": 3, "b": 1}], + expected=[{"_id": 1, "a": "hello"}], + msg="$exists: true AND $type: string", + ), + QueryTestCase( + id="exists_and_in", + filter={"a": {"$exists": True, "$in": [1, 2, 3]}}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": 5}, {"_id": 3, "b": 1}], + expected=[{"_id": 1, "a": 1}], + msg="$exists: true AND $in: [1, 2, 3]", + ), + QueryTestCase( + id="exists_and_nin", + filter={"a": {"$exists": True, "$nin": [1, 2]}}, + doc=[{"_id": 1, "a": 1}, {"_id": 2, "a": None}, {"_id": 3, "b": 1}], + expected=[{"_id": 2, "a": None}], + msg="$exists: true AND $nin: [1, 2] — null field matches", + ), +] + +REGEX_SIZE_ALL_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="exists_and_regex", + filter={"a": {"$exists": True, "$regex": "^h"}}, + doc=[{"_id": 1, "a": "hello"}, {"_id": 2, "a": 123}, {"_id": 3, "b": 1}], + expected=[{"_id": 1, "a": "hello"}], + msg="$exists: true AND $regex matches", + ), + QueryTestCase( + id="exists_and_size", + filter={"a": {"$exists": True, "$size": 2}}, + doc=[{"_id": 1, "a": [1, 2]}, {"_id": 2, "a": [1]}, {"_id": 3, "b": 1}], + expected=[{"_id": 1, "a": [1, 2]}], + msg="$exists: true AND $size: 2", + ), + QueryTestCase( + id="exists_and_all", + filter={"a": {"$exists": True, "$all": [1, 2]}}, + doc=[{"_id": 1, "a": [1, 2, 3]}, {"_id": 2, "a": [1, 2]}, {"_id": 3, "b": 1}], + expected=[{"_id": 1, "a": [1, 2, 3]}, {"_id": 2, "a": [1, 2]}], + msg="$exists: true AND $all: [1, 2]", + ), +] + +NOT_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="not_exists_true", + filter={"a": {"$not": {"$exists": True}}}, + doc=DOCS, + expected=[{"_id": 3, "b": 1}], + msg="$not {$exists: true} equivalent to $exists: false", + ), + QueryTestCase( + id="not_exists_false", + filter={"a": {"$not": {"$exists": False}}}, + doc=DOCS, + expected=[{"_id": 1, "a": 1}, {"_id": 2, "a": None}], + msg="$not {$exists: false} equivalent to $exists: true", + ), +] + +LOGICAL_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="or_exists_false_or_gt", + filter={"$or": [{"a": {"$exists": False}}, {"a": {"$gt": 10}}]}, + doc=[{"_id": 1, "a": 5}, {"_id": 2, "a": 15}, {"_id": 3, "b": 1}], + expected=[{"_id": 2, "a": 15}, {"_id": 3, "b": 1}], + msg="$or with $exists: false or $gt: 10", + ), + QueryTestCase( + id="nor_exists_true", + filter={"$nor": [{"a": {"$exists": True}}]}, + doc=DOCS, + expected=[{"_id": 3, "b": 1}], + msg="$nor with $exists: true — equivalent to $exists: false", + ), + QueryTestCase( + id="or_overlapping", + filter={"$or": [{"a": {"$exists": True}}, {"b": {"$exists": True}}]}, + doc=[ + {"_id": 1, "a": 1, "b": 2}, + {"_id": 2, "a": 1}, + {"_id": 3, "b": 2}, + {"_id": 4, "c": 3}, + ], + expected=[ + {"_id": 1, "a": 1, "b": 2}, + {"_id": 2, "a": 1}, + {"_id": 3, "b": 2}, + ], + msg="$or with overlapping $exists conditions", + ), + QueryTestCase( + id="and_both_fields_exist", + filter={"$and": [{"a": {"$exists": True}}, {"b": {"$exists": True}}]}, + doc=[ + {"_id": 1, "a": 1, "b": 2}, + {"_id": 2, "a": 1}, + {"_id": 3, "b": 2}, + {"_id": 4, "c": 3}, + ], + expected=[{"_id": 1, "a": 1, "b": 2}], + msg="$and with both fields must exist", + ), +] + +ALL_TESTS = COMPARISON_TESTS + REGEX_SIZE_ALL_TESTS + NOT_TESTS + LOGICAL_TESTS + + +@pytest.mark.parametrize("test", pytest_params(ALL_TESTS)) +def test_exists_operator_combinations(collection, test): + """Parametrized test for $exists combined with other operators.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_other_commands.py b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_other_commands.py new file mode 100644 index 00000000..078cc4cb --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_other_commands.py @@ -0,0 +1,292 @@ +""" +Tests for $exists in non-find command contexts. + +Covers aggregate $match, aggregate pipeline interaction ($addFields, $project, $unset), +$exists not supported in expressions, count, deleteMany, updateMany, +and $exists after update operations ($unset, $rename, $setOnInsert). +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertFailureCode, assertSuccess +from documentdb_tests.framework.error_codes import ( + PROJECT_UNKNOWN_EXPRESSION_ERROR, + UNRECOGNIZED_EXPRESSION_ERROR, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +DOCS = [ + {"_id": 1, "a": 10}, + {"_id": 2, "a": 5}, + {"_id": 3, "b": 1}, +] + + +AGG_MATCH_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="agg_match_true", + filter={"a": {"$exists": True}}, + doc=DOCS, + expected=[{"_id": 1, "a": 10}, {"_id": 2, "a": 5}], + msg="$exists: true in aggregate $match — parity with find", + ), + QueryTestCase( + id="agg_match_false", + filter={"a": {"$exists": False}}, + doc=DOCS, + expected=[{"_id": 3, "b": 1}], + msg="$exists: false in aggregate $match — parity with find", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(AGG_MATCH_TESTS)) +def test_exists_aggregate_match(collection, test): + """Parametrized test for $exists in aggregate $match.""" + collection.insert_many(test.doc) + result = execute_command( + collection, + {"aggregate": collection.name, "pipeline": [{"$match": test.filter}], "cursor": {}}, + ) + assertSuccess(result, test.expected, ignore_doc_order=True) + + +COUNT_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="count_true", + filter={"a": {"$exists": True}}, + doc=DOCS, + expected=2, + msg="count with $exists: true", + ), + QueryTestCase( + id="count_false", + filter={"a": {"$exists": False}}, + doc=DOCS, + expected=1, + msg="count with $exists: false", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(COUNT_TESTS)) +def test_exists_count(collection, test): + """Parametrized test for count with $exists.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"count": collection.name, "query": test.filter}) + assertSuccess(result, {"n": test.expected}, raw_res=True, transform=lambda r: {"n": r["n"]}) + + +def test_exists_match_after_addFields(collection): + """$exists: true in $match after $addFields creates field.""" + collection.insert_many([{"_id": 1, "b": 1}]) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + {"$addFields": {"a": 1}}, + {"$match": {"a": {"$exists": True}}}, + ], + "cursor": {}, + }, + ) + assertSuccess(result, [{"_id": 1, "a": 1, "b": 1}]) + + +def test_exists_match_after_project_removes(collection): + """$exists: false in $match after $project removes field.""" + collection.insert_many([{"_id": 1, "a": 1, "b": 2}]) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + {"$project": {"a": 0}}, + {"$match": {"a": {"$exists": False}}}, + ], + "cursor": {}, + }, + ) + assertSuccess(result, [{"_id": 1, "b": 2}]) + + +def test_exists_match_after_unset(collection): + """$exists: false in $match after $unset removes field.""" + collection.insert_many([{"_id": 1, "a": 1, "b": 2}]) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + {"$unset": "a"}, + {"$match": {"a": {"$exists": False}}}, + ], + "cursor": {}, + }, + ) + assertSuccess(result, [{"_id": 1, "b": 2}]) + + +def test_exists_not_in_project_expression(collection): + """$exists in $project expression errors with code 31325.""" + collection.insert_many([{"_id": 1, "a": 1}]) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + {"$project": {"result": {"$exists": "$a"}}}, + ], + "cursor": {}, + }, + ) + assertFailureCode(result, PROJECT_UNKNOWN_EXPRESSION_ERROR) + + +def test_exists_not_in_addFields_expression(collection): + """$exists in $addFields expression errors with code 168.""" + collection.insert_many([{"_id": 1, "a": 1}]) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + {"$addFields": {"result": {"$exists": "$a"}}}, + ], + "cursor": {}, + }, + ) + assertFailureCode(result, UNRECOGNIZED_EXPRESSION_ERROR) + + +def test_exists_not_in_match_expr(collection): + """$exists in $match $expr errors with code 168.""" + collection.insert_many([{"_id": 1, "a": 1}]) + result = execute_command( + collection, + { + "aggregate": collection.name, + "pipeline": [ + {"$match": {"$expr": {"$exists": "$a"}}}, + ], + "cursor": {}, + }, + ) + assertFailureCode(result, UNRECOGNIZED_EXPRESSION_ERROR) + + +def test_exists_delete_many(collection): + """deleteMany with $exists: false removes docs without field.""" + collection.insert_many(DOCS) + execute_command( + collection, + {"delete": collection.name, "deletes": [{"q": {"a": {"$exists": False}}, "limit": 0}]}, + ) + result = execute_command(collection, {"find": collection.name, "filter": {}}) + assertSuccess(result, [{"_id": 1, "a": 10}, {"_id": 2, "a": 5}], ignore_doc_order=True) + + +def test_exists_update_many(collection): + """updateMany with $exists: false sets default value.""" + collection.insert_many(DOCS) + execute_command( + collection, + { + "update": collection.name, + "updates": [ + {"q": {"a": {"$exists": False}}, "u": {"$set": {"a": "default"}}, "multi": True}, + ], + }, + ) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": 3}}) + assertSuccess(result, [{"_id": 3, "a": "default", "b": 1}]) + + +def test_exists_after_unset_false(collection): + """$exists: false matches after $unset removes field.""" + collection.insert_many([{"_id": 1, "a": 1, "b": 2}]) + execute_command( + collection, + {"update": collection.name, "updates": [{"q": {}, "u": {"$unset": {"a": ""}}}]}, + ) + result = execute_command( + collection, {"find": collection.name, "filter": {"a": {"$exists": False}}} + ) + assertSuccess(result, [{"_id": 1, "b": 2}]) + + +def test_exists_after_unset_no_longer_true(collection): + """$exists: true no longer matches after $unset.""" + collection.insert_many([{"_id": 1, "a": 1, "b": 2}]) + execute_command( + collection, + {"update": collection.name, "updates": [{"q": {}, "u": {"$unset": {"a": ""}}}]}, + ) + result = execute_command( + collection, {"find": collection.name, "filter": {"a": {"$exists": True}}} + ) + assertSuccess(result, []) + + +def test_exists_after_rename_old_name(collection): + """$exists: false matches old name after $rename.""" + collection.insert_many([{"_id": 1, "a": 1, "b": 2}]) + execute_command( + collection, + {"update": collection.name, "updates": [{"q": {}, "u": {"$rename": {"a": "c"}}}]}, + ) + result = execute_command( + collection, {"find": collection.name, "filter": {"a": {"$exists": False}}} + ) + assertSuccess(result, [{"_id": 1, "b": 2, "c": 1}]) + + +def test_exists_after_rename_new_name(collection): + """$exists: true matches new name after $rename.""" + collection.insert_many([{"_id": 1, "a": 1, "b": 2}]) + execute_command( + collection, + {"update": collection.name, "updates": [{"q": {}, "u": {"$rename": {"a": "c"}}}]}, + ) + result = execute_command( + collection, {"find": collection.name, "filter": {"c": {"$exists": True}}} + ) + assertSuccess(result, [{"_id": 1, "b": 2, "c": 1}]) + + +def test_exists_after_setOnInsert_upsert(collection): + """$exists: true matches after $setOnInsert with upsert.""" + execute_command( + collection, + { + "update": collection.name, + "updates": [ + {"q": {"_id": 1}, "u": {"$setOnInsert": {"a": 1}}, "upsert": True}, + ], + }, + ) + result = execute_command( + collection, {"find": collection.name, "filter": {"a": {"$exists": True}}} + ) + assertSuccess(result, [{"_id": 1, "a": 1}]) + + +def test_exists_update_filter_sets_default(collection): + """updateMany with $exists: false filter sets default value.""" + collection.insert_many([{"_id": 1, "a": 1}, {"_id": 2, "b": 2}]) + execute_command( + collection, + { + "update": collection.name, + "updates": [ + {"q": {"a": {"$exists": False}}, "u": {"$set": {"a": "default"}}, "multi": True}, + ], + }, + ) + result = execute_command(collection, {"find": collection.name, "filter": {"_id": 2}}) + assertSuccess(result, [{"_id": 2, "a": "default", "b": 2}]) diff --git a/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_query_contexts.py b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_query_contexts.py new file mode 100644 index 00000000..2fc3b480 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/query/misc/exists/test_exists_query_contexts.py @@ -0,0 +1,134 @@ +""" +Tests for $exists in find query contexts and algebraic properties. + +Covers find filter, cursor operations (sort, limit, projection), +complementarity, idempotency, and contradiction. +""" + +from dataclasses import dataclass +from typing import Any, Optional + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.query.utils.query_test_case import ( + QueryTestCase, +) +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.test_case import BaseTestCase + +DOCS: list[dict] = [ + {"_id": 1, "a": 10}, + {"_id": 2, "a": 5}, + {"_id": 3, "b": 1}, +] + +ALGEBRAIC_DOCS: list[dict] = [ + {"_id": 1, "a": 1}, + {"_id": 2, "a": None}, + {"_id": 3, "b": 1}, + {"_id": 4}, +] + + +@dataclass(frozen=True) +class FindTestCase(BaseTestCase): + """Test case for find with optional sort/limit/projection.""" + + filter: Any = None + doc: Any = None + sort: Optional[dict] = None + limit: Optional[int] = None + projection: Optional[dict] = None + + +FIND_TESTS: list[FindTestCase] = [ + FindTestCase( + id="find_with_sort", + filter={"a": {"$exists": True}}, + doc=DOCS, + sort={"a": 1}, + expected=[{"_id": 2, "a": 5}, {"_id": 1, "a": 10}], + msg="$exists: true with sort", + ), + FindTestCase( + id="find_with_limit", + filter={"a": {"$exists": True}}, + doc=DOCS, + sort={"_id": 1}, + limit=1, + expected=[{"_id": 1, "a": 10}], + msg="$exists: true with limit", + ), + FindTestCase( + id="find_with_projection", + filter={"a": {"$exists": True}}, + doc=DOCS, + projection={"a": 1, "_id": 0}, + expected=[{"a": 10}, {"a": 5}], + msg="$exists: true with projection", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(FIND_TESTS)) +def test_exists_query_contexts(collection, test): + """Parametrized test for $exists in find query contexts.""" + collection.insert_many(test.doc) + cmd = {"find": collection.name, "filter": test.filter} + if test.sort: + cmd["sort"] = test.sort + if test.limit: + cmd["limit"] = test.limit + if test.projection: + cmd["projection"] = test.projection + result = execute_command(collection, cmd) + assertSuccess(result, test.expected, ignore_doc_order=test.sort is None) + + +ALGEBRAIC_TESTS: list[QueryTestCase] = [ + QueryTestCase( + id="partition_true", + filter={"a": {"$exists": True}}, + doc=ALGEBRAIC_DOCS, + expected=[{"_id": 1, "a": 1}, {"_id": 2, "a": None}], + msg="$exists: true returns docs with field (partition half)", + ), + QueryTestCase( + id="partition_false", + filter={"a": {"$exists": False}}, + doc=ALGEBRAIC_DOCS, + expected=[{"_id": 3, "b": 1}, {"_id": 4}], + msg="$exists: false returns docs without field (partition half)", + ), + QueryTestCase( + id="no_overlap", + filter={"$and": [{"a": {"$exists": True}}, {"a": {"$exists": False}}]}, + doc=ALGEBRAIC_DOCS, + expected=[], + msg="$exists: true AND false is empty (no overlap)", + ), + QueryTestCase( + id="idempotency_true", + filter={"$and": [{"a": {"$exists": True}}, {"a": {"$exists": True}}]}, + doc=ALGEBRAIC_DOCS, + expected=[{"_id": 1, "a": 1}, {"_id": 2, "a": None}], + msg="Duplicate $exists: true is same as single", + ), + QueryTestCase( + id="idempotency_false", + filter={"$and": [{"a": {"$exists": False}}, {"a": {"$exists": False}}]}, + doc=ALGEBRAIC_DOCS, + expected=[{"_id": 3, "b": 1}, {"_id": 4}], + msg="Duplicate $exists: false is same as single", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ALGEBRAIC_TESTS)) +def test_exists_algebraic_properties(collection, test): + """Parametrized test for $exists algebraic properties.""" + collection.insert_many(test.doc) + result = execute_command(collection, {"find": collection.name, "filter": test.filter}) + assertSuccess(result, test.expected, ignore_doc_order=True)