From 1cd44316e15ac4de74ae229c071f9c8b8138d06b Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 2 Jun 2026 09:51:39 -0700 Subject: [PATCH 1/4] Add and refine tests for Positional ($) array update operator Signed-off-by: PatersonProjects --- .../tests/core/operator/update/__init__.py | 0 .../update/array/positional/__init__.py | 0 .../array/positional/test_positional_core.py | 97 ++++++++++ .../positional/test_positional_data_types.py | 175 ++++++++++++++++++ .../test_positional_embedded_docs.py | 83 +++++++++ .../positional/test_positional_errors.py | 82 ++++++++ .../update/test_update_array_integration.py | 95 ++++++++++ .../core/operator/update/utils/__init__.py | 3 + .../operator/update/utils/update_test_case.py | 16 ++ 9 files changed, 551 insertions(+) create mode 100644 documentdb_tests/compatibility/tests/core/operator/update/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/update/array/positional/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_core.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_data_types.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_embedded_docs.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/update/test_update_array_integration.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/update/utils/__init__.py create mode 100644 documentdb_tests/compatibility/tests/core/operator/update/utils/update_test_case.py diff --git a/documentdb_tests/compatibility/tests/core/operator/update/__init__.py b/documentdb_tests/compatibility/tests/core/operator/update/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/update/array/positional/__init__.py b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_core.py b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_core.py new file mode 100644 index 000000000..226472e73 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_core.py @@ -0,0 +1,97 @@ +"""Tests for $ positional update operator core behavior. + +Covers: first-match semantics, update command integration, upsert with +existing doc, and empty array edge case. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.update.utils import UpdateTestCase +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +CHANGED_DOC_TESTS: list[UpdateTestCase] = [ + UpdateTestCase( + "set_first_match", + setup_docs=[{"_id": 1, "arr": [1, 2, 3, 2, 4]}], + query={"_id": 1, "arr": 2}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [1, 99, 3, 2, 4]}, + msg="$ should update only the first matching element", + ), + UpdateTestCase( + "match_last_element", + setup_docs=[{"_id": 1, "arr": [10, 20, 30]}], + query={"_id": 1, "arr": 30}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [10, 20, 99]}, + msg="$ should update last element when it matches", + ), + UpdateTestCase( + "upsert_doc_exists_succeeds", + setup_docs=[{"_id": 1, "arr": [1, 2, 3]}], + query={"_id": 1, "arr": 2}, + update={"$set": {"arr.$": 99}}, + upsert=True, + expected={"_id": 1, "arr": [1, 99, 3]}, + msg="$ in upsert when document exists and matches should work normally", + ), +] + + +UPDATE_RESULT_TESTS: list[UpdateTestCase] = [ + UpdateTestCase( + "update_one", + setup_docs=[{"_id": 1, "arr": [1, 2, 3]}, {"_id": 2, "arr": [2, 3, 4]}], + query={"arr": 2}, + update={"$set": {"arr.$": 99}}, + expected={"n": 1, "nModified": 1, "ok": 1.0}, + msg="$ should work with updateOne (updates only one matched doc)", + ), + UpdateTestCase( + "array_field_missing", + setup_docs=[{"_id": 1, "x": 1}], + query={"_id": 1, "arr": 5}, + update={"$set": {"arr.$": 99}}, + expected={"n": 0, "nModified": 0, "ok": 1.0}, + msg="$ when array field is missing should result in no match", + ), + UpdateTestCase( + "empty_array_no_match", + setup_docs=[{"_id": 1, "arr": []}], + query={"_id": 1, "arr": 5}, + update={"$set": {"arr.$": 99}}, + expected={"n": 0, "nModified": 0, "ok": 1.0}, + msg="$ on empty array with no match should result in no update", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(CHANGED_DOC_TESTS)) +def test_positional_changed_doc(collection, test: UpdateTestCase): + """Test $ positional update operator produces expected document.""" + if test.setup_docs: + collection.insert_many(test.setup_docs) + + update_doc = {"q": test.query, "u": test.update} + if test.upsert: + update_doc["upsert"] = True + execute_command(collection, {"update": collection.name, "updates": [update_doc]}) + + result = execute_command( + collection, {"find": collection.name, "filter": {"_id": test.expected["_id"]}} + ) + assertSuccess(result, [test.expected], msg=test.msg) + + +@pytest.mark.parametrize("test", pytest_params(UPDATE_RESULT_TESTS)) +def test_positional_update_result(collection, test: UpdateTestCase): + """Test $ positional update command returns expected n/nModified.""" + if test.setup_docs: + collection.insert_many(test.setup_docs) + + result = execute_command( + collection, {"update": collection.name, "updates": [{"q": test.query, "u": test.update}]} + ) + assertSuccess(result, test.expected, msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_data_types.py b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_data_types.py new file mode 100644 index 000000000..7b919a780 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_data_types.py @@ -0,0 +1,175 @@ +"""Tests for $ positional with data type coverage. + +Covers: all BSON types as array elements, numeric equivalence in matching, +BSON type distinction, and per-input-position coverage. +""" + +import pytest +from bson import Decimal128, Int64 + +from documentdb_tests.compatibility.tests.core.operator.update.utils import UpdateTestCase +from documentdb_tests.framework.assertions import assertProperties, assertSuccess +from documentdb_tests.framework.bson_type_validator import ( + BsonTypeTestCase, + generate_bson_acceptance_test_cases, +) +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params +from documentdb_tests.framework.property_checks import Eq +from documentdb_tests.framework.test_constants import BsonType + +POSITIONAL_BSON_TYPE_SPEC = [ + BsonTypeTestCase( + id="positional_match", + msg="$ should match and update array elements of all BSON types", + keyword="arr", + valid_types=list(BsonType), + ), +] + +BSON_TYPE_CASES = generate_bson_acceptance_test_cases(POSITIONAL_BSON_TYPE_SPEC) + + +@pytest.mark.parametrize("bson_type,sample_value,spec", BSON_TYPE_CASES) +def test_positional_bson_types(collection, bson_type, sample_value, spec): + """Test $ positional matches and updates each BSON type in an array.""" + collection.insert_one({"_id": 1, "arr": [sample_value, sample_value]}) + + execute_command( + collection, + { + "update": collection.name, + "updates": [ + {"q": {"_id": 1, "arr": sample_value}, "u": {"$set": {"arr.$": "replaced"}}} + ], + }, + ) + + result = execute_command(collection, {"find": collection.name, "filter": {"_id": 1}}) + assertProperties(result, {"arr.0": Eq("replaced")}, msg=spec.msg) + + +NUMERIC_EQUIVALENCE_TESTS: list[UpdateTestCase] = [ + UpdateTestCase( + "int_matches_long", + setup_docs=[{"_id": 1, "arr": [Int64(1), Int64(2), Int64(3)]}], + query={"_id": 1, "arr": 1}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [99, Int64(2), Int64(3)]}, + msg="$ query with int should match long element (numeric equivalence)", + ), + UpdateTestCase( + "double_matches_int", + setup_docs=[{"_id": 1, "arr": [1, 2, 3]}], + query={"_id": 1, "arr": 1.0}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [99, 2, 3]}, + msg="$ query with double(1.0) should match int(1)", + ), + UpdateTestCase( + "decimal128_matches_int", + setup_docs=[{"_id": 1, "arr": [1, 2, 3]}], + query={"_id": 1, "arr": Decimal128("1")}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [99, 2, 3]}, + msg="$ query with Decimal128('1') should match int(1)", + ), + UpdateTestCase( + "long_matches_double", + setup_docs=[{"_id": 1, "arr": [1.0, 2.0, 3.0]}], + query={"_id": 1, "arr": Int64(2)}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [1.0, 99, 3.0]}, + msg="$ query with long should match double element (numeric equivalence)", + ), + UpdateTestCase( + "negative_zero_matches_zero", + setup_docs=[{"_id": 1, "arr": [0, 1, 2]}], + query={"_id": 1, "arr": -0.0}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [99, 1, 2]}, + msg="$ query with -0.0 should match 0 (negative zero equivalence)", + ), + UpdateTestCase( + "null_matches_null", + setup_docs=[{"_id": 1, "arr": [1, None, 3]}], + query={"_id": 1, "arr": None}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [1, 99, 3]}, + msg="$ query with null should match null element in array", + ), +] + + +BSON_DISTINCTION_TESTS: list[UpdateTestCase] = [ + UpdateTestCase( + "false_not_match_zero", + setup_docs=[{"_id": 1, "arr": [0, 1, 2]}], + query={"_id": 1, "arr": False}, + update={"$set": {"arr.$": 99}}, + expected={"n": 0, "nModified": 0, "ok": 1.0}, + msg="$ query with false should NOT match int(0) (distinct BSON types)", + ), + UpdateTestCase( + "true_not_match_one", + setup_docs=[{"_id": 1, "arr": [0, 1, 2]}], + query={"_id": 1, "arr": True}, + update={"$set": {"arr.$": 99}}, + expected={"n": 0, "nModified": 0, "ok": 1.0}, + msg="$ query with true should NOT match int(1) (distinct BSON types)", + ), + UpdateTestCase( + "null_not_match_zero", + setup_docs=[{"_id": 1, "arr": [0, 1, 2]}], + query={"_id": 1, "arr": None}, + update={"$set": {"arr.$": 99}}, + expected={"n": 0, "nModified": 0, "ok": 1.0}, + msg="$ query with null should NOT match int(0) (distinct BSON types)", + ), + UpdateTestCase( + "string_not_match_int", + setup_docs=[{"_id": 1, "arr": [1, 2, 3]}], + query={"_id": 1, "arr": "1"}, + update={"$set": {"arr.$": 99}}, + expected={"n": 0, "nModified": 0, "ok": 1.0}, + msg="$ query with string '1' should NOT match int(1) (distinct BSON types)", + ), + UpdateTestCase( + "null_not_match_missing", + setup_docs=[{"_id": 1, "arr": [1, 2, 3]}], + query={"_id": 1, "arr": None}, + update={"$set": {"arr.$": 99}}, + expected={"n": 0, "nModified": 0, "ok": 1.0}, + msg="$ query with null should NOT match when array has no null element", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(NUMERIC_EQUIVALENCE_TESTS)) +def test_positional_numeric_equivalence(collection, test: UpdateTestCase): + """Test $ positional with numeric equivalence across BSON number types.""" + if test.setup_docs: + collection.insert_many(test.setup_docs) + + execute_command( + collection, + {"update": collection.name, "updates": [{"q": test.query, "u": test.update}]}, + ) + + result = execute_command( + collection, {"find": collection.name, "filter": {"_id": test.expected["_id"]}} + ) + assertSuccess(result, [test.expected], msg=test.msg) + + +@pytest.mark.parametrize("test", pytest_params(BSON_DISTINCTION_TESTS)) +def test_positional_bson_type_distinction(collection, test: UpdateTestCase): + """Test $ positional does not match across distinct BSON types.""" + if test.setup_docs: + collection.insert_many(test.setup_docs) + + result = execute_command( + collection, + {"update": collection.name, "updates": [{"q": test.query, "u": test.update}]}, + ) + assertSuccess(result, test.expected, msg=test.msg, raw_res=True) diff --git a/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_embedded_docs.py b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_embedded_docs.py new file mode 100644 index 000000000..69b8b42b4 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_embedded_docs.py @@ -0,0 +1,83 @@ +"""Tests for $ positional with embedded documents and dot notation. + +Covers: updating fields in embedded documents, dot notation paths, +nested fields, and edge cases with numeric path components. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.update.utils import UpdateTestCase +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +EMBEDDED_DOC_TESTS: list[UpdateTestCase] = [ + UpdateTestCase( + "set_field_in_matched_doc", + setup_docs=[{"_id": 1, "arr": [{"name": "A", "val": 1}, {"name": "B", "val": 2}]}], + query={"_id": 1, "arr.name": "B"}, + update={"$set": {"arr.$.val": 99}}, + expected={"_id": 1, "arr": [{"name": "A", "val": 1}, {"name": "B", "val": 99}]}, + msg="$ with dot notation should update field in matched embedded document", + ), + UpdateTestCase( + "elemMatch_set_field", + setup_docs=[{"_id": 1, "arr": [{"x": 1, "y": 10}, {"x": 2, "y": 20}]}], + query={"_id": 1, "arr": {"$elemMatch": {"x": 2}}}, + update={"$set": {"arr.$.y": 99}}, + expected={"_id": 1, "arr": [{"x": 1, "y": 10}, {"x": 2, "y": 99}]}, + msg="$ with $elemMatch and $set should update matched embedded doc field", + ), + UpdateTestCase( + "deeply_nested_field", + setup_docs=[{"_id": 1, "arr": [{"nested": {"field": 1}}, {"nested": {"field": 2}}]}], + query={"_id": 1, "arr.nested.field": 2}, + update={"$set": {"arr.$.nested.field": 99}}, + expected={"_id": 1, "arr": [{"nested": {"field": 1}}, {"nested": {"field": 99}}]}, + msg="$ updating deeply nested field should work", + ), + UpdateTestCase( + "nested_array_field", + setup_docs=[{"_id": 1, "outer": {"inner": [1, 2, 3]}}], + query={"_id": 1, "outer.inner": 2}, + update={"$set": {"outer.inner.$": 99}}, + expected={"_id": 1, "outer": {"inner": [1, 99, 3]}}, + msg="$ on nested array 'outer.inner.$' should update matched element", + ), + UpdateTestCase( + "query_traverses_nested_array", + setup_docs=[ + {"_id": 1, "arr": [{"nested": [1, 2]}, {"nested": [3, 4]}]}, + ], + query={"arr.nested": 3}, + update={"$set": {"arr.$.nested": [99]}}, + expected={"_id": 1, "arr": [{"nested": [1, 2]}, {"nested": [99]}]}, + msg="$ should resolve outer array position when query traverses inner array", + ), + UpdateTestCase( + "numeric_path_component", + setup_docs=[ + {"_id": 1, "arr": [{"vals": [10, 20]}, {"vals": [30, 40]}]}, + ], + query={"_id": 1, "arr.vals": 30}, + update={"$set": {"arr.$.vals.0": 99}}, + expected={"_id": 1, "arr": [{"vals": [10, 20]}, {"vals": [99, 40]}]}, + msg="$ with numeric path component should update specific index in matched element", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(EMBEDDED_DOC_TESTS)) +def test_positional_embedded_docs(collection, test: UpdateTestCase): + """Test $ positional with embedded documents and dot notation.""" + if test.setup_docs: + collection.insert_many(test.setup_docs) + + execute_command( + collection, {"update": collection.name, "updates": [{"q": test.query, "u": test.update}]} + ) + + result = execute_command( + collection, {"find": collection.name, "filter": {"_id": test.expected["_id"]}} + ) + assertSuccess(result, [test.expected], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py new file mode 100644 index 000000000..819bf09fa --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py @@ -0,0 +1,82 @@ +"""Tests for $ positional update operator error cases. + +Covers: missing array field in query, upsert restriction, negation operators, +and multiple positional operators in update path. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.update.utils import UpdateTestCase +from documentdb_tests.framework.assertions import assertFailureCode +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 + +ERROR_TESTS: list[UpdateTestCase] = [ + UpdateTestCase( + "no_array_field_in_query", + setup_docs=[{"_id": 1, "arr": [1, 2, 3]}], + query={"_id": 1}, + update={"$set": {"arr.$": 99}}, + error_code=BAD_VALUE_ERROR, + msg="$ without array field in query should fail with BadValue", + ), + UpdateTestCase( + "upsert_no_match_fails", + setup_docs=None, + query={"arr": 5}, + update={"$set": {"arr.$": 99}}, + upsert=True, + error_code=BAD_VALUE_ERROR, + msg="$ in upsert when no document exists should fail (cannot determine position)", + ), + UpdateTestCase( + "ne_on_array_field", + setup_docs=[{"_id": 1, "arr": [1, 2, 3]}], + query={"_id": 1, "arr": {"$ne": 5}}, + update={"$set": {"arr.$": 99}}, + error_code=BAD_VALUE_ERROR, + msg="$ with $ne on array field should fail", + ), + UpdateTestCase( + "not_on_array_field", + setup_docs=[{"_id": 1, "arr": [1, 2, 3]}], + query={"_id": 1, "arr": {"$not": {"$eq": 5}}}, + update={"$set": {"arr.$": 99}}, + error_code=BAD_VALUE_ERROR, + msg="$ with $not on array field should fail", + ), + UpdateTestCase( + "nin_on_array_field", + setup_docs=[{"_id": 1, "arr": [1, 2, 3]}], + query={"_id": 1, "arr": {"$nin": [5, 6]}}, + update={"$set": {"arr.$": 99}}, + error_code=BAD_VALUE_ERROR, + msg="$ with $nin on array field should fail", + ), + UpdateTestCase( + "multiple_positional_in_path", + setup_docs=[ + {"_id": 1, "arr": [{"items": [1, 2]}, {"items": [3, 4]}]}, + ], + query={"arr.items": 3}, + update={"$set": {"arr.$.items.$": 99}}, + error_code=BAD_VALUE_ERROR, + msg="$ used twice in update path should fail (cannot traverse multiple arrays)", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(ERROR_TESTS)) +def test_positional_errors(collection, test: UpdateTestCase): + """Test $ positional update operator error cases.""" + if test.setup_docs: + collection.insert_many(test.setup_docs) + + update_doc = {"q": test.query, "u": test.update} + if test.upsert: + update_doc["upsert"] = True + command = {"update": collection.name, "updates": [update_doc]} + result = execute_command(collection, command) + assert test.error_code is not None + assertFailureCode(result, test.error_code, msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/update/test_update_array_integration.py b/documentdb_tests/compatibility/tests/core/operator/update/test_update_array_integration.py new file mode 100644 index 000000000..596749360 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/update/test_update_array_integration.py @@ -0,0 +1,95 @@ +"""Integration tests for array update operators with query operators. + +Tests that verify interactions between array update operators and various +query operators, ensuring correct element matching and update behavior. +""" + +import pytest + +from documentdb_tests.compatibility.tests.core.operator.update.utils import UpdateTestCase +from documentdb_tests.framework.assertions import assertSuccess +from documentdb_tests.framework.executor import execute_command +from documentdb_tests.framework.parametrize import pytest_params + +POSITIONAL_INTEGRATION_TESTS: list[UpdateTestCase] = [ + UpdateTestCase( + "gt_condition", + setup_docs=[{"_id": 1, "arr": [5, 15, 25]}], + query={"_id": 1, "arr": {"$gt": 10}}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [5, 99, 25]}, + msg="$ with $gt should match first element > value", + ), + UpdateTestCase( + "lt_condition", + setup_docs=[{"_id": 1, "arr": [5, 15, 25]}], + query={"_id": 1, "arr": {"$lt": 20}}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [99, 15, 25]}, + msg="$ with $lt should match first element < value", + ), + UpdateTestCase( + "gte_condition", + setup_docs=[{"_id": 1, "arr": [5, 15, 25]}], + query={"_id": 1, "arr": {"$gte": 15}}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [5, 99, 25]}, + msg="$ with $gte should match first element >= value", + ), + UpdateTestCase( + "lte_condition", + setup_docs=[{"_id": 1, "arr": [5, 15, 25]}], + query={"_id": 1, "arr": {"$lte": 15}}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [99, 15, 25]}, + msg="$ with $lte should match first element <= value", + ), + UpdateTestCase( + "in_condition", + setup_docs=[{"_id": 1, "arr": [5, 15, 25]}], + query={"_id": 1, "arr": {"$in": [15, 25]}}, + update={"$set": {"arr.$": 99}}, + expected={"_id": 1, "arr": [5, 99, 25]}, + msg="$ with $in should match first element in list", + ), + UpdateTestCase( + "elemMatch_comparison_operators", + setup_docs=[{"_id": 1, "arr": [{"v": 5}, {"v": 15}, {"v": 25}]}], + query={"_id": 1, "arr": {"$elemMatch": {"v": {"$gt": 10, "$lt": 20}}}}, + update={"$set": {"arr.$.v": 99}}, + expected={"_id": 1, "arr": [{"v": 5}, {"v": 99}, {"v": 25}]}, + msg="$ with $elemMatch containing comparison operators should match correct position", + ), + UpdateTestCase( + "elemMatch_with_regex", + setup_docs=[{"_id": 1, "arr": [{"s": "abc"}, {"s": "xyz"}, {"s": "def"}]}], + query={"_id": 1, "arr": {"$elemMatch": {"s": {"$regex": "^x"}}}}, + update={"$set": {"arr.$.s": "matched"}}, + expected={"_id": 1, "arr": [{"s": "abc"}, {"s": "matched"}, {"s": "def"}]}, + msg="$ with $elemMatch containing $regex should match correct position", + ), + UpdateTestCase( + "negation_inside_elemMatch", + setup_docs=[{"_id": 1, "arr": [{"x": 1}, {"x": 2}, {"x": 3}]}], + query={"_id": 1, "arr": {"$elemMatch": {"x": {"$ne": 1}}}}, + update={"$set": {"arr.$.x": 99}}, + expected={"_id": 1, "arr": [{"x": 1}, {"x": 99}, {"x": 3}]}, + msg="$ with negation inside $elemMatch should succeed", + ), +] + + +@pytest.mark.parametrize("test", pytest_params(POSITIONAL_INTEGRATION_TESTS)) +def test_positional_query_operators(collection, test: UpdateTestCase): + """Test $ positional with various query operators.""" + if test.setup_docs: + collection.insert_many(test.setup_docs) + + execute_command( + collection, {"update": collection.name, "updates": [{"q": test.query, "u": test.update}]} + ) + + result = execute_command( + collection, {"find": collection.name, "filter": {"_id": test.expected["_id"]}} + ) + assertSuccess(result, [test.expected], msg=test.msg) diff --git a/documentdb_tests/compatibility/tests/core/operator/update/utils/__init__.py b/documentdb_tests/compatibility/tests/core/operator/update/utils/__init__.py new file mode 100644 index 000000000..3a406803b --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/update/utils/__init__.py @@ -0,0 +1,3 @@ +from documentdb_tests.compatibility.tests.core.operator.update.utils.update_test_case import ( # noqa: E501, F401 + UpdateTestCase, +) diff --git a/documentdb_tests/compatibility/tests/core/operator/update/utils/update_test_case.py b/documentdb_tests/compatibility/tests/core/operator/update/utils/update_test_case.py new file mode 100644 index 000000000..504da4a87 --- /dev/null +++ b/documentdb_tests/compatibility/tests/core/operator/update/utils/update_test_case.py @@ -0,0 +1,16 @@ +"""Shared test case for update operator tests.""" + +from dataclasses import dataclass +from typing import Any + +from documentdb_tests.framework.test_case import BaseTestCase + + +@dataclass(frozen=True) +class UpdateTestCase(BaseTestCase): + """Test case for update operator tests.""" + + setup_docs: Any = None + query: Any = None + update: Any = None + upsert: bool = False From c8cac130f4954f03f79dd8bf51ef05aac39a48a4 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 2 Jun 2026 10:24:58 -0700 Subject: [PATCH 2/4] PR test run fixes Signed-off-by: PatersonProjects --- .../operator/update/array/positional/test_positional_errors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py index 819bf09fa..960b4caf5 100644 --- a/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py @@ -68,7 +68,7 @@ @pytest.mark.parametrize("test", pytest_params(ERROR_TESTS)) -def test_positional_errors(collection, test: UpdateTestCase): +def test_positional_errors(collection, test): """Test $ positional update operator error cases.""" if test.setup_docs: collection.insert_many(test.setup_docs) @@ -78,5 +78,4 @@ def test_positional_errors(collection, test: UpdateTestCase): update_doc["upsert"] = True command = {"update": collection.name, "updates": [update_doc]} result = execute_command(collection, command) - assert test.error_code is not None assertFailureCode(result, test.error_code, msg=test.msg) From 4ee26d687b5a26f89a7796594dee43346c62f3c6 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 2 Jun 2026 10:32:21 -0700 Subject: [PATCH 3/4] Added missing init Signed-off-by: PatersonProjects --- .../compatibility/tests/core/operator/update/array/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 documentdb_tests/compatibility/tests/core/operator/update/array/__init__.py diff --git a/documentdb_tests/compatibility/tests/core/operator/update/array/__init__.py b/documentdb_tests/compatibility/tests/core/operator/update/array/__init__.py new file mode 100644 index 000000000..e69de29bb From 5b334e6d521e31fc9de6f53741d61ab236b42287 Mon Sep 17 00:00:00 2001 From: PatersonProjects Date: Tue, 2 Jun 2026 16:15:16 -0700 Subject: [PATCH 4/4] Added non-array existing test Signed-off-by: PatersonProjects --- .../update/array/positional/test_positional_errors.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py index 960b4caf5..06f7ff7ab 100644 --- a/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py +++ b/documentdb_tests/compatibility/tests/core/operator/update/array/positional/test_positional_errors.py @@ -64,6 +64,14 @@ error_code=BAD_VALUE_ERROR, msg="$ used twice in update path should fail (cannot traverse multiple arrays)", ), + UpdateTestCase( + "field_exists_but_not_array", + setup_docs=[{"_id": 1, "arr": 5}], + query={"_id": 1, "arr": 5}, + update={"$set": {"arr.$": 99}}, + error_code=BAD_VALUE_ERROR, + msg="$ on scalar field should fail even when query matches", + ), ]