From f7de5cd453fa7fde551c36e0408a261c83766c23 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 30 Sep 2024 08:09:06 -0500 Subject: [PATCH 1/8] PYTHON-4576 Allow update to supply sort option --- pymongo/asynchronous/collection.py | 29 +- pymongo/operations.py | 26 +- pymongo/synchronous/collection.py | 27 +- .../unified/bulkWrite-replaceOne-sort.json | 239 ++++++++++++++++ .../unified/bulkWrite-updateOne-sort.json | 255 ++++++++++++++++++ test/crud/unified/replaceOne-sort.json | 232 ++++++++++++++++ test/crud/unified/updateOne-sort.json | 240 +++++++++++++++++ test/utils.py | 7 +- 8 files changed, 1045 insertions(+), 10 deletions(-) create mode 100644 test/crud/unified/bulkWrite-replaceOne-sort.json create mode 100644 test/crud/unified/bulkWrite-updateOne-sort.json create mode 100644 test/crud/unified/replaceOne-sort.json create mode 100644 test/crud/unified/updateOne-sort.json diff --git a/pymongo/asynchronous/collection.py b/pymongo/asynchronous/collection.py index 1ec74aad02..cd31514a64 100644 --- a/pymongo/asynchronous/collection.py +++ b/pymongo/asynchronous/collection.py @@ -684,6 +684,7 @@ async def bulk_write( session: Optional[AsyncClientSession] = None, comment: Optional[Any] = None, let: Optional[Mapping] = None, + sort: Optional[Mapping] = None, ) -> BulkWriteResult: """Send a batch of write operations to the server. @@ -738,6 +739,8 @@ async def bulk_write( constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. :return: An instance of :class:`~pymongo.results.BulkWriteResult`. @@ -746,6 +749,9 @@ async def bulk_write( .. note:: `bypass_document_validation` requires server version **>= 3.2** + .. versionchanged:: 4.10 + Added ``sort`` parameter. + .. versionchanged:: 4.1 Added ``comment`` parameter. Added ``let`` parameter. @@ -760,7 +766,9 @@ async def bulk_write( """ common.validate_list("requests", requests) - blk = _AsyncBulk(self, ordered, bypass_document_validation, comment=comment, let=let) + blk = _AsyncBulk( + self, ordered, bypass_document_validation, comment=comment, let=let, sort=sort + ) for request in requests: try: request._add_to_bulk(blk) @@ -972,6 +980,7 @@ async def _update( session: Optional[AsyncClientSession] = None, retryable_write: bool = False, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> Optional[Mapping[str, Any]]: """Internal update / replace helper.""" @@ -1008,6 +1017,10 @@ async def _update( common.validate_is_mapping("let", let) command["let"] = let + if sort is not None: + common.validate_is_mapping("sort", sort) + command["sort"] = sort + if comment is not None: command["comment"] = comment # Update command. @@ -1058,6 +1071,7 @@ async def _update_retryable( hint: Optional[_IndexKeyHint] = None, session: Optional[AsyncClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> Optional[Mapping[str, Any]]: """Internal update / replace helper.""" @@ -1081,6 +1095,7 @@ async def _update( session=session, retryable_write=retryable_write, let=let, + sort=sort, comment=comment, ) @@ -1101,6 +1116,7 @@ async def replace_one( hint: Optional[_IndexKeyHint] = None, session: Optional[AsyncClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> UpdateResult: """Replace a single document matching the filter. @@ -1155,8 +1171,12 @@ async def replace_one( aggregate expression context (e.g. "$$var"). :param comment: A user-provided comment to attach to this command. + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. :return: - An instance of :class:`~pymongo.results.UpdateResult`. + .. versionchanged:: 4.10 + Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. Added ``comment`` parameter. @@ -1188,6 +1208,7 @@ async def replace_one( hint=hint, session=session, let=let, + sort=sort, comment=comment, ), write_concern.acknowledged, @@ -1204,6 +1225,7 @@ async def update_one( hint: Optional[_IndexKeyHint] = None, session: Optional[AsyncClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> UpdateResult: """Update a single document matching the filter. @@ -1262,11 +1284,15 @@ async def update_one( constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. :param comment: A user-provided comment to attach to this command. :return: - An instance of :class:`~pymongo.results.UpdateResult`. + .. versionchanged:: 4.10 + Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. Added ``comment`` parameter. @@ -1301,6 +1327,7 @@ async def update_one( hint=hint, session=session, let=let, + sort=sort, comment=comment, ), write_concern.acknowledged, diff --git a/pymongo/operations.py b/pymongo/operations.py index d2e1feba69..4c07d39656 100644 --- a/pymongo/operations.py +++ b/pymongo/operations.py @@ -335,6 +335,7 @@ def __init__( collation: Optional[_CollationIn] = None, hint: Optional[_IndexKeyHint] = None, namespace: Optional[str] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create a ReplaceOne instance. @@ -353,8 +354,12 @@ def __init__( :meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` or :meth:`~pymongo.collection.Collection.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.2 and above. + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. :param namespace: (optional) The namespace in which to replace a document. + .. versionchanged:: 4.10 + Added ``sort`` option. .. versionchanged:: 4.9 Added the `namespace` option to support `MongoClient.bulk_write`. .. versionchanged:: 3.11 @@ -456,6 +461,7 @@ class _UpdateOp: "_array_filters", "_hint", "_namespace", + "_sort", ) def __init__( @@ -467,6 +473,7 @@ def __init__( array_filters: Optional[list[Mapping[str, Any]]], hint: Optional[_IndexKeyHint], namespace: Optional[str], + sort: Optional[Mapping[str, Any]], ): if filter is not None: validate_is_mapping("filter", filter) @@ -478,6 +485,8 @@ def __init__( self._hint: Union[str, dict[str, Any], None] = helpers_shared._index_document(hint) else: self._hint = hint + if sort is not None: + self._sort = sort self._filter = filter self._doc = doc @@ -512,7 +521,7 @@ def __ne__(self, other: Any) -> bool: def __repr__(self) -> str: if self._namespace: - return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( + return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( self.__class__.__name__, self._filter, self._doc, @@ -520,9 +529,10 @@ def __repr__(self) -> str: self._collation, self._array_filters, self._hint, + self._sort, self._namespace, ) - return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( + return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( self.__class__.__name__, self._filter, self._doc, @@ -530,6 +540,7 @@ def __repr__(self) -> str: self._collation, self._array_filters, self._hint, + self._sort, ) @@ -547,6 +558,7 @@ def __init__( array_filters: Optional[list[Mapping[str, Any]]] = None, hint: Optional[_IndexKeyHint] = None, namespace: Optional[str] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Represents an update_one operation. @@ -567,8 +579,12 @@ def __init__( :meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` or :meth:`~pymongo.collection.Collection.create_index` (e.g. ``[('field', ASCENDING)]``). This option is only supported on MongoDB 4.2 and above. - :param namespace: (optional) The namespace in which to update a document. + :param namespace: The namespace in which to update a document. + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. + .. versionchanged:: 4.10 + Added ``sort`` option. .. versionchanged:: 4.9 Added the `namespace` option to support `MongoClient.bulk_write`. .. versionchanged:: 3.11 @@ -580,7 +596,7 @@ def __init__( .. versionchanged:: 3.5 Added the `collation` option. """ - super().__init__(filter, update, upsert, collation, array_filters, hint, namespace) + super().__init__(filter, update, upsert, collation, array_filters, hint, namespace, sort) def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None: """Add this operation to the _AsyncBulk/_Bulk instance `bulkobj`.""" @@ -659,7 +675,7 @@ def __init__( .. versionchanged:: 3.5 Added the `collation` option. """ - super().__init__(filter, update, upsert, collation, array_filters, hint, namespace) + super().__init__(filter, update, upsert, collation, array_filters, hint, namespace, None) def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None: """Add this operation to the _AsyncBulk/_Bulk instance `bulkobj`.""" diff --git a/pymongo/synchronous/collection.py b/pymongo/synchronous/collection.py index 7a41aef31f..532e8b60c6 100644 --- a/pymongo/synchronous/collection.py +++ b/pymongo/synchronous/collection.py @@ -683,6 +683,7 @@ def bulk_write( session: Optional[ClientSession] = None, comment: Optional[Any] = None, let: Optional[Mapping] = None, + sort: Optional[Mapping] = None, ) -> BulkWriteResult: """Send a batch of write operations to the server. @@ -737,6 +738,8 @@ def bulk_write( constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. :return: An instance of :class:`~pymongo.results.BulkWriteResult`. @@ -745,6 +748,9 @@ def bulk_write( .. note:: `bypass_document_validation` requires server version **>= 3.2** + .. versionchanged:: 4.10 + Added ``sort`` parameter. + .. versionchanged:: 4.1 Added ``comment`` parameter. Added ``let`` parameter. @@ -759,7 +765,7 @@ def bulk_write( """ common.validate_list("requests", requests) - blk = _Bulk(self, ordered, bypass_document_validation, comment=comment, let=let) + blk = _Bulk(self, ordered, bypass_document_validation, comment=comment, let=let, sort=sort) for request in requests: try: request._add_to_bulk(blk) @@ -971,6 +977,7 @@ def _update( session: Optional[ClientSession] = None, retryable_write: bool = False, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> Optional[Mapping[str, Any]]: """Internal update / replace helper.""" @@ -1007,6 +1014,10 @@ def _update( common.validate_is_mapping("let", let) command["let"] = let + if sort is not None: + common.validate_is_mapping("sort", sort) + command["sort"] = sort + if comment is not None: command["comment"] = comment # Update command. @@ -1057,6 +1068,7 @@ def _update_retryable( hint: Optional[_IndexKeyHint] = None, session: Optional[ClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> Optional[Mapping[str, Any]]: """Internal update / replace helper.""" @@ -1080,6 +1092,7 @@ def _update( session=session, retryable_write=retryable_write, let=let, + sort=sort, comment=comment, ) @@ -1100,6 +1113,7 @@ def replace_one( hint: Optional[_IndexKeyHint] = None, session: Optional[ClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> UpdateResult: """Replace a single document matching the filter. @@ -1154,8 +1168,12 @@ def replace_one( aggregate expression context (e.g. "$$var"). :param comment: A user-provided comment to attach to this command. + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. :return: - An instance of :class:`~pymongo.results.UpdateResult`. + .. versionchanged:: 4.10 + Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. Added ``comment`` parameter. @@ -1187,6 +1205,7 @@ def replace_one( hint=hint, session=session, let=let, + sort=sort, comment=comment, ), write_concern.acknowledged, @@ -1203,6 +1222,7 @@ def update_one( hint: Optional[_IndexKeyHint] = None, session: Optional[ClientSession] = None, let: Optional[Mapping[str, Any]] = None, + sort: Optional[Mapping[str, Any]] = None, comment: Optional[Any] = None, ) -> UpdateResult: """Update a single document matching the filter. @@ -1261,11 +1281,15 @@ def update_one( constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). + :param sort: Specify which document the operation updates if the query matches + multiple documents. The first document matched by the sort order will be updated. :param comment: A user-provided comment to attach to this command. :return: - An instance of :class:`~pymongo.results.UpdateResult`. + .. versionchanged:: 4.10 + Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. Added ``comment`` parameter. @@ -1300,6 +1324,7 @@ def update_one( hint=hint, session=session, let=let, + sort=sort, comment=comment, ), write_concern.acknowledged, diff --git a/test/crud/unified/bulkWrite-replaceOne-sort.json b/test/crud/unified/bulkWrite-replaceOne-sort.json new file mode 100644 index 0000000000..c0bd383514 --- /dev/null +++ b/test/crud/unified/bulkWrite-replaceOne-sort.json @@ -0,0 +1,239 @@ +{ + "description": "BulkWrite replaceOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "BulkWrite replaceOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "replaceOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + }, + { + "description": "BulkWrite replaceOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "replaceOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + } + } + ] + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/bulkWrite-updateOne-sort.json b/test/crud/unified/bulkWrite-updateOne-sort.json new file mode 100644 index 0000000000..f78bd3bf3e --- /dev/null +++ b/test/crud/unified/bulkWrite-updateOne-sort.json @@ -0,0 +1,255 @@ +{ + "description": "BulkWrite updateOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "BulkWrite updateOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "updateOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": [ + { + "$set": { + "x": 1 + } + } + ] + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": [ + { + "$set": { + "x": 1 + } + } + ], + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + }, + { + "description": "BulkWrite updateOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "updateOne": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": [ + { + "$set": { + "x": 1 + } + } + ] + } + } + ] + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": [ + { + "$set": { + "x": 1 + } + } + ], + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/replaceOne-sort.json b/test/crud/unified/replaceOne-sort.json new file mode 100644 index 0000000000..cf2271dda5 --- /dev/null +++ b/test/crud/unified/replaceOne-sort.json @@ -0,0 +1,232 @@ +{ + "description": "replaceOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "ReplaceOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "name": "replaceOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + }, + { + "description": "replaceOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "name": "replaceOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/updateOne-sort.json b/test/crud/unified/updateOne-sort.json new file mode 100644 index 0000000000..8fe4f50b94 --- /dev/null +++ b/test/crud/unified/updateOne-sort.json @@ -0,0 +1,240 @@ +{ + "description": "updateOne-sort", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "UpdateOne with sort option", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "operations": [ + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": { + "$inc": { + "x": 1 + } + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 34 + } + ] + } + ] + }, + { + "description": "updateOne with sort option unsupported (server-side error)", + "runOnRequirements": [ + { + "maxServerVersion": "7.99" + } + ], + "operations": [ + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": { + "$inc": { + "x": 1 + } + } + }, + "expectError": { + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/test/utils.py b/test/utils.py index 6eefd1c7ea..1b0a44aac3 100644 --- a/test/utils.py +++ b/test/utils.py @@ -1077,10 +1077,11 @@ def parse_spec_options(opts): def prepare_spec_arguments(spec, arguments, opname, entity_map, with_txn_callback): for arg_name in list(arguments): c2s = camel_to_snake(arg_name) - # PyMongo accepts sort as list of tuples. + # PyMongo accepts sort as list of tuples or as an optional dict for update/replaceOne. if arg_name == "sort": - sort_dict = arguments[arg_name] - arguments[arg_name] = list(sort_dict.items()) + if spec["name"] not in ["replaceOne", "updateOne"]: + sort_dict = arguments[arg_name] + arguments[arg_name] = list(sort_dict.items()) # Named "key" instead not fieldName. if arg_name == "fieldName": arguments["key"] = arguments.pop(arg_name) From 523875b6fed18d6adb77fb70ab0f5e0be254a6fc Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 30 Sep 2024 08:44:32 -0500 Subject: [PATCH 2/8] clean up bulk support --- pymongo/asynchronous/bulk.py | 13 +++++++++++++ pymongo/asynchronous/client_bulk.py | 9 +++++++++ pymongo/asynchronous/collection.py | 22 +++++++++------------- pymongo/synchronous/bulk.py | 13 +++++++++++++ pymongo/synchronous/client_bulk.py | 9 +++++++++ pymongo/synchronous/collection.py | 20 +++++++++----------- 6 files changed, 62 insertions(+), 24 deletions(-) diff --git a/pymongo/asynchronous/bulk.py b/pymongo/asynchronous/bulk.py index 9d33a990ed..e6cfe5b36e 100644 --- a/pymongo/asynchronous/bulk.py +++ b/pymongo/asynchronous/bulk.py @@ -109,6 +109,7 @@ def __init__( self.uses_array_filters = False self.uses_hint_update = False self.uses_hint_delete = False + self.uses_sort = False self.is_retryable = True self.retrying = False self.started_retryable_write = False @@ -144,6 +145,7 @@ def add_update( collation: Optional[Mapping[str, Any]] = None, array_filters: Optional[list[Mapping[str, Any]]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create an update document and add it to the list of ops.""" validate_ok_for_update(update) @@ -159,6 +161,9 @@ def add_update( if hint is not None: self.uses_hint_update = True cmd["hint"] = hint + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort if multi: # A bulk_write containing an update_many is not retryable. self.is_retryable = False @@ -171,6 +176,7 @@ def add_replace( upsert: bool = False, collation: Optional[Mapping[str, Any]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create a replace document and add it to the list of ops.""" validate_ok_for_replace(replacement) @@ -181,6 +187,9 @@ def add_replace( if hint is not None: self.uses_hint_update = True cmd["hint"] = hint + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort self.ops.append((_UPDATE, cmd)) def add_delete( @@ -699,6 +708,10 @@ async def execute_no_results( raise ConfigurationError( "Must be connected to MongoDB 4.2+ to use hint on unacknowledged update commands." ) + if unack and self.uses_sort and conn.max_wire_version < 25: + raise ConfigurationError( + "Must be connected to MongoDB 8.0+ to use sort on unacknowledged update commands." + ) # Cannot have both unacknowledged writes and bypass document validation. if self.bypass_doc_val: raise OperationFailure( diff --git a/pymongo/asynchronous/client_bulk.py b/pymongo/asynchronous/client_bulk.py index dc800c9549..96571c21eb 100644 --- a/pymongo/asynchronous/client_bulk.py +++ b/pymongo/asynchronous/client_bulk.py @@ -118,6 +118,7 @@ def __init__( self.uses_array_filters = False self.uses_hint_update = False self.uses_hint_delete = False + self.uses_sort = False self.is_retryable = self.client.options.retry_writes self.retrying = False @@ -148,6 +149,7 @@ def add_update( collation: Optional[Mapping[str, Any]] = None, array_filters: Optional[list[Mapping[str, Any]]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create an update document and add it to the list of ops.""" validate_ok_for_update(update) @@ -169,6 +171,9 @@ def add_update( if collation is not None: self.uses_collation = True cmd["collation"] = collation + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort if multi: # A bulk_write containing an update_many is not retryable. self.is_retryable = False @@ -184,6 +189,7 @@ def add_replace( upsert: Optional[bool] = None, collation: Optional[Mapping[str, Any]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create a replace document and add it to the list of ops.""" validate_ok_for_replace(replacement) @@ -202,6 +208,9 @@ def add_replace( if collation is not None: self.uses_collation = True cmd["collation"] = collation + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort self.ops.append(("replace", cmd)) self.namespaces.append(namespace) self.total_ops += 1 diff --git a/pymongo/asynchronous/collection.py b/pymongo/asynchronous/collection.py index cd31514a64..60993ea7d0 100644 --- a/pymongo/asynchronous/collection.py +++ b/pymongo/asynchronous/collection.py @@ -684,7 +684,6 @@ async def bulk_write( session: Optional[AsyncClientSession] = None, comment: Optional[Any] = None, let: Optional[Mapping] = None, - sort: Optional[Mapping] = None, ) -> BulkWriteResult: """Send a batch of write operations to the server. @@ -739,8 +738,6 @@ async def bulk_write( constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). - :param sort: Specify which document the operation updates if the query matches - multiple documents. The first document matched by the sort order will be updated. :return: An instance of :class:`~pymongo.results.BulkWriteResult`. @@ -749,9 +746,6 @@ async def bulk_write( .. note:: `bypass_document_validation` requires server version **>= 3.2** - .. versionchanged:: 4.10 - Added ``sort`` parameter. - .. versionchanged:: 4.1 Added ``comment`` parameter. Added ``let`` parameter. @@ -766,9 +760,7 @@ async def bulk_write( """ common.validate_list("requests", requests) - blk = _AsyncBulk( - self, ordered, bypass_document_validation, comment=comment, let=let, sort=sort - ) + blk = _AsyncBulk(self, ordered, bypass_document_validation, comment=comment, let=let) for request in requests: try: request._add_to_bulk(blk) @@ -1012,15 +1004,19 @@ async def _update( if not isinstance(hint, str): hint = helpers_shared._index_document(hint) update_doc["hint"] = hint + if sort is not None: + if not acknowledged and conn.max_wire_version < 25: + raise ConfigurationError( + "Must be connected to MongoDB 8.0+ to use sort on unacknowledged update commands." + ) + common.validate_is_mapping("sort", sort) + update_doc["sort"] = sort + command = {"update": self.name, "ordered": ordered, "updates": [update_doc]} if let is not None: common.validate_is_mapping("let", let) command["let"] = let - if sort is not None: - common.validate_is_mapping("sort", sort) - command["sort"] = sort - if comment is not None: command["comment"] = comment # Update command. diff --git a/pymongo/synchronous/bulk.py b/pymongo/synchronous/bulk.py index c658157ea1..7fb29a977f 100644 --- a/pymongo/synchronous/bulk.py +++ b/pymongo/synchronous/bulk.py @@ -109,6 +109,7 @@ def __init__( self.uses_array_filters = False self.uses_hint_update = False self.uses_hint_delete = False + self.uses_sort = False self.is_retryable = True self.retrying = False self.started_retryable_write = False @@ -144,6 +145,7 @@ def add_update( collation: Optional[Mapping[str, Any]] = None, array_filters: Optional[list[Mapping[str, Any]]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create an update document and add it to the list of ops.""" validate_ok_for_update(update) @@ -159,6 +161,9 @@ def add_update( if hint is not None: self.uses_hint_update = True cmd["hint"] = hint + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort if multi: # A bulk_write containing an update_many is not retryable. self.is_retryable = False @@ -171,6 +176,7 @@ def add_replace( upsert: bool = False, collation: Optional[Mapping[str, Any]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create a replace document and add it to the list of ops.""" validate_ok_for_replace(replacement) @@ -181,6 +187,9 @@ def add_replace( if hint is not None: self.uses_hint_update = True cmd["hint"] = hint + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort self.ops.append((_UPDATE, cmd)) def add_delete( @@ -697,6 +706,10 @@ def execute_no_results( raise ConfigurationError( "Must be connected to MongoDB 4.2+ to use hint on unacknowledged update commands." ) + if unack and self.uses_sort and conn.max_wire_version < 25: + raise ConfigurationError( + "Must be connected to MongoDB 8.0+ to use sort on unacknowledged update commands." + ) # Cannot have both unacknowledged writes and bypass document validation. if self.bypass_doc_val: raise OperationFailure( diff --git a/pymongo/synchronous/client_bulk.py b/pymongo/synchronous/client_bulk.py index f41f0203f2..2c38b1d76c 100644 --- a/pymongo/synchronous/client_bulk.py +++ b/pymongo/synchronous/client_bulk.py @@ -118,6 +118,7 @@ def __init__( self.uses_array_filters = False self.uses_hint_update = False self.uses_hint_delete = False + self.uses_sort = False self.is_retryable = self.client.options.retry_writes self.retrying = False @@ -148,6 +149,7 @@ def add_update( collation: Optional[Mapping[str, Any]] = None, array_filters: Optional[list[Mapping[str, Any]]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create an update document and add it to the list of ops.""" validate_ok_for_update(update) @@ -169,6 +171,9 @@ def add_update( if collation is not None: self.uses_collation = True cmd["collation"] = collation + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort if multi: # A bulk_write containing an update_many is not retryable. self.is_retryable = False @@ -184,6 +189,7 @@ def add_replace( upsert: Optional[bool] = None, collation: Optional[Mapping[str, Any]] = None, hint: Union[str, dict[str, Any], None] = None, + sort: Optional[Mapping[str, Any]] = None, ) -> None: """Create a replace document and add it to the list of ops.""" validate_ok_for_replace(replacement) @@ -202,6 +208,9 @@ def add_replace( if collation is not None: self.uses_collation = True cmd["collation"] = collation + if sort is not None: + self.uses_sort = True + cmd["sort"] = sort self.ops.append(("replace", cmd)) self.namespaces.append(namespace) self.total_ops += 1 diff --git a/pymongo/synchronous/collection.py b/pymongo/synchronous/collection.py index 532e8b60c6..31aeb4d6ae 100644 --- a/pymongo/synchronous/collection.py +++ b/pymongo/synchronous/collection.py @@ -683,7 +683,6 @@ def bulk_write( session: Optional[ClientSession] = None, comment: Optional[Any] = None, let: Optional[Mapping] = None, - sort: Optional[Mapping] = None, ) -> BulkWriteResult: """Send a batch of write operations to the server. @@ -738,8 +737,6 @@ def bulk_write( constant or closed expressions that do not reference document fields. Parameters can then be accessed as variables in an aggregate expression context (e.g. "$$var"). - :param sort: Specify which document the operation updates if the query matches - multiple documents. The first document matched by the sort order will be updated. :return: An instance of :class:`~pymongo.results.BulkWriteResult`. @@ -748,9 +745,6 @@ def bulk_write( .. note:: `bypass_document_validation` requires server version **>= 3.2** - .. versionchanged:: 4.10 - Added ``sort`` parameter. - .. versionchanged:: 4.1 Added ``comment`` parameter. Added ``let`` parameter. @@ -765,7 +759,7 @@ def bulk_write( """ common.validate_list("requests", requests) - blk = _Bulk(self, ordered, bypass_document_validation, comment=comment, let=let, sort=sort) + blk = _Bulk(self, ordered, bypass_document_validation, comment=comment, let=let) for request in requests: try: request._add_to_bulk(blk) @@ -1009,15 +1003,19 @@ def _update( if not isinstance(hint, str): hint = helpers_shared._index_document(hint) update_doc["hint"] = hint + if sort is not None: + if not acknowledged and conn.max_wire_version < 25: + raise ConfigurationError( + "Must be connected to MongoDB 8.0+ to use sort on unacknowledged update commands." + ) + common.validate_is_mapping("sort", sort) + update_doc["sort"] = sort + command = {"update": self.name, "ordered": ordered, "updates": [update_doc]} if let is not None: common.validate_is_mapping("let", let) command["let"] = let - if sort is not None: - common.validate_is_mapping("sort", sort) - command["sort"] = sort - if comment is not None: command["comment"] = comment # Update command. From 2402cdf1c710b9d3f5ee77d25285e82700176a08 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 30 Sep 2024 08:48:37 -0500 Subject: [PATCH 3/8] clean up operations --- pymongo/operations.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/pymongo/operations.py b/pymongo/operations.py index 4c07d39656..986174dfa1 100644 --- a/pymongo/operations.py +++ b/pymongo/operations.py @@ -376,6 +376,7 @@ def __init__( else: self._hint = hint + self._sort = sort self._filter = filter self._doc = replacement self._upsert = upsert @@ -390,6 +391,7 @@ def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None: self._upsert, collation=validate_collation_or_none(self._collation), hint=self._hint, + sort=self._sort, ) def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None: @@ -405,6 +407,7 @@ def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None: self._upsert, collation=validate_collation_or_none(self._collation), hint=self._hint, + sort=self._sort, ) def __eq__(self, other: Any) -> bool: @@ -416,13 +419,15 @@ def __eq__(self, other: Any) -> bool: other._collation, other._hint, other._namespace, + other._sort, ) == ( self._filter, self._doc, self._upsert, self._collation, - other._hint, + self._hint, self._namespace, + self._sort, ) return NotImplemented @@ -431,7 +436,7 @@ def __ne__(self, other: Any) -> bool: def __repr__(self) -> str: if self._namespace: - return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( + return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( self.__class__.__name__, self._filter, self._doc, @@ -439,14 +444,16 @@ def __repr__(self) -> str: self._collation, self._hint, self._namespace, + self._sort, ) - return "{}({!r}, {!r}, {!r}, {!r}, {!r})".format( + return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( self.__class__.__name__, self._filter, self._doc, self._upsert, self._collation, self._hint, + self._sort, ) @@ -485,15 +492,13 @@ def __init__( self._hint: Union[str, dict[str, Any], None] = helpers_shared._index_document(hint) else: self._hint = hint - if sort is not None: - self._sort = sort - self._filter = filter self._doc = doc self._upsert = upsert self._collation = collation self._array_filters = array_filters self._namespace = namespace + self._sort = sort def __eq__(self, other: object) -> bool: if isinstance(other, type(self)): @@ -505,6 +510,7 @@ def __eq__(self, other: object) -> bool: other._array_filters, other._hint, other._namespace, + other._sort, ) == ( self._filter, self._doc, @@ -513,6 +519,7 @@ def __eq__(self, other: object) -> bool: self._array_filters, self._hint, self._namespace, + self._sort, ) return NotImplemented @@ -529,8 +536,8 @@ def __repr__(self) -> str: self._collation, self._array_filters, self._hint, - self._sort, self._namespace, + self._sort, ) return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format( self.__class__.__name__, @@ -608,6 +615,7 @@ def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None: collation=validate_collation_or_none(self._collation), array_filters=self._array_filters, hint=self._hint, + sort=self._sort, ) def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None: @@ -625,6 +633,7 @@ def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None: collation=validate_collation_or_none(self._collation), array_filters=self._array_filters, hint=self._hint, + sort=self._sort, ) From b63db31a82c9a7695a00759bfc7fa1feab65348c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 30 Sep 2024 09:10:06 -0500 Subject: [PATCH 4/8] fix replaceone --- pymongo/operations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pymongo/operations.py b/pymongo/operations.py index 986174dfa1..580aebb682 100644 --- a/pymongo/operations.py +++ b/pymongo/operations.py @@ -325,6 +325,7 @@ class ReplaceOne(Generic[_DocumentType]): "_collation", "_hint", "_namespace", + "_sort", ) def __init__( From ac2264fdb285cb4580ee8d3c9ca77325198667d9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 30 Sep 2024 10:57:23 -0500 Subject: [PATCH 5/8] try with updated specs --- .../aggregate-write-readPreference.json | 69 --- .../client-bulkWrite-partialResults.json | 540 ++++++++++++++++++ .../client-bulkWrite-replaceOne-sort.json | 159 ++++++ .../client-bulkWrite-updateOne-sort.json | 163 ++++++ .../db-aggregate-write-readPreference.json | 51 -- 5 files changed, 862 insertions(+), 120 deletions(-) create mode 100644 test/crud/unified/client-bulkWrite-partialResults.json create mode 100644 test/crud/unified/client-bulkWrite-replaceOne-sort.json create mode 100644 test/crud/unified/client-bulkWrite-updateOne-sort.json diff --git a/test/crud/unified/aggregate-write-readPreference.json b/test/crud/unified/aggregate-write-readPreference.json index bc887e83cb..c1fa3b4574 100644 --- a/test/crud/unified/aggregate-write-readPreference.json +++ b/test/crud/unified/aggregate-write-readPreference.json @@ -78,11 +78,6 @@ "x": 33 } ] - }, - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [] } ], "tests": [ @@ -159,22 +154,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] }, { @@ -250,22 +229,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] }, { @@ -344,22 +307,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] }, { @@ -438,22 +385,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll1", - "databaseName": "db0", - "documents": [ - { - "_id": 2, - "x": 22 - }, - { - "_id": 3, - "x": 33 - } - ] - } ] } ] diff --git a/test/crud/unified/client-bulkWrite-partialResults.json b/test/crud/unified/client-bulkWrite-partialResults.json new file mode 100644 index 0000000000..b35e94a2ea --- /dev/null +++ b/test/crud/unified/client-bulkWrite-partialResults.json @@ -0,0 +1,540 @@ +{ + "description": "client bulkWrite partial results", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0" + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0", + "newDocument": { + "_id": 2, + "x": 22 + } + }, + "tests": [ + { + "description": "partialResult is unset when first operation fails during an ordered bulk write (verbose)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "ordered": true, + "verboseResults": true + }, + "expectError": { + "expectResult": { + "$$unsetOrMatches": { + "insertedCount": { + "$$exists": false + }, + "upsertedCount": { + "$$exists": false + }, + "matchedCount": { + "$$exists": false + }, + "modifiedCount": { + "$$exists": false + }, + "deletedCount": { + "$$exists": false + }, + "insertResults": { + "$$exists": false + }, + "updateResults": { + "$$exists": false + }, + "deleteResults": { + "$$exists": false + } + } + } + } + } + ] + }, + { + "description": "partialResult is unset when first operation fails during an ordered bulk write (summary)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "ordered": true, + "verboseResults": false + }, + "expectError": { + "expectResult": { + "$$unsetOrMatches": { + "insertedCount": { + "$$exists": false + }, + "upsertedCount": { + "$$exists": false + }, + "matchedCount": { + "$$exists": false + }, + "modifiedCount": { + "$$exists": false + }, + "deletedCount": { + "$$exists": false + }, + "insertResults": { + "$$exists": false + }, + "updateResults": { + "$$exists": false + }, + "deleteResults": { + "$$exists": false + } + } + } + } + } + ] + }, + { + "description": "partialResult is set when second operation fails during an ordered bulk write (verbose)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "ordered": true, + "verboseResults": true + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 2 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + } + ] + }, + { + "description": "partialResult is set when second operation fails during an ordered bulk write (summary)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "ordered": true, + "verboseResults": false + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "$$unsetOrMatches": {} + }, + "updateResults": { + "$$unsetOrMatches": {} + }, + "deleteResults": { + "$$unsetOrMatches": {} + } + } + } + } + ] + }, + { + "description": "partialResult is unset when all operations fail during an unordered bulk write", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "ordered": false + }, + "expectError": { + "expectResult": { + "$$unsetOrMatches": { + "insertedCount": { + "$$exists": false + }, + "upsertedCount": { + "$$exists": false + }, + "matchedCount": { + "$$exists": false + }, + "modifiedCount": { + "$$exists": false + }, + "deletedCount": { + "$$exists": false + }, + "insertResults": { + "$$exists": false + }, + "updateResults": { + "$$exists": false + }, + "deleteResults": { + "$$exists": false + } + } + } + } + } + ] + }, + { + "description": "partialResult is set when first operation fails during an unordered bulk write (verbose)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "ordered": false, + "verboseResults": true + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "1": { + "insertedId": 2 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + } + ] + }, + { + "description": "partialResult is set when first operation fails during an unordered bulk write (summary)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + } + ], + "ordered": false, + "verboseResults": false + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "$$unsetOrMatches": {} + }, + "updateResults": { + "$$unsetOrMatches": {} + }, + "deleteResults": { + "$$unsetOrMatches": {} + } + } + } + } + ] + }, + { + "description": "partialResult is set when second operation fails during an unordered bulk write (verbose)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "ordered": false, + "verboseResults": true + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 2 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + } + ] + }, + { + "description": "partialResult is set when first operation fails during an unordered bulk write (summary)", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 2, + "x": 22 + } + } + }, + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "ordered": false, + "verboseResults": false + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "$$unsetOrMatches": {} + }, + "updateResults": { + "$$unsetOrMatches": {} + }, + "deleteResults": { + "$$unsetOrMatches": {} + } + } + } + } + ] + } + ] +} diff --git a/test/crud/unified/client-bulkWrite-replaceOne-sort.json b/test/crud/unified/client-bulkWrite-replaceOne-sort.json new file mode 100644 index 0000000000..515bceae84 --- /dev/null +++ b/test/crud/unified/client-bulkWrite-replaceOne-sort.json @@ -0,0 +1,159 @@ +{ + "description": "client bulkWrite updateOne-sort", + "schemaVersion": "1.0", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "client bulkWrite replaceOne with sort option", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "replaceOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "ops": [ + { + "update": 0, + "filter": { + "_id": { + "$gt": 1 + } + }, + "updateMods": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "nErrors": 0, + "nMatched": 1, + "nModified": 1 + }, + "commandName": "bulkWrite" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/client-bulkWrite-updateOne-sort.json b/test/crud/unified/client-bulkWrite-updateOne-sort.json new file mode 100644 index 0000000000..688bb01027 --- /dev/null +++ b/test/crud/unified/client-bulkWrite-updateOne-sort.json @@ -0,0 +1,163 @@ +{ + "description": "client bulkWrite updateOne-sort", + "schemaVersion": "1.0", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "client bulkWrite updateOne with sort option", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "ops": [ + { + "update": 0, + "filter": { + "_id": { + "$gt": 1 + } + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "nErrors": 0, + "nMatched": 1, + "nModified": 1 + }, + "commandName": "bulkWrite" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 34 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/db-aggregate-write-readPreference.json b/test/crud/unified/db-aggregate-write-readPreference.json index 2a81282de8..b6460f001f 100644 --- a/test/crud/unified/db-aggregate-write-readPreference.json +++ b/test/crud/unified/db-aggregate-write-readPreference.json @@ -52,13 +52,6 @@ } } ], - "initialData": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [] - } - ], "tests": [ { "description": "Database-level aggregate with $out includes read preference for 5.0+ server", @@ -141,17 +134,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] }, { @@ -235,17 +217,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] }, { @@ -332,17 +303,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] }, { @@ -429,17 +389,6 @@ } ] } - ], - "outcome": [ - { - "collectionName": "coll0", - "databaseName": "db0", - "documents": [ - { - "_id": 1 - } - ] - } ] } ] From 7e46c4ab05a7eda7f09f49b09ba378fbc76b92c4 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 1 Oct 2024 12:23:57 -0500 Subject: [PATCH 6/8] update tests --- test/crud/unified/client-bulkWrite-replaceOne-sort.json | 5 ++++- test/crud/unified/client-bulkWrite-updateOne-sort.json | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/crud/unified/client-bulkWrite-replaceOne-sort.json b/test/crud/unified/client-bulkWrite-replaceOne-sort.json index 515bceae84..53218c1f48 100644 --- a/test/crud/unified/client-bulkWrite-replaceOne-sort.json +++ b/test/crud/unified/client-bulkWrite-replaceOne-sort.json @@ -1,6 +1,6 @@ { "description": "client bulkWrite updateOne-sort", - "schemaVersion": "1.0", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "8.0" @@ -51,6 +51,9 @@ ] } ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0" + }, "tests": [ { "description": "client bulkWrite replaceOne with sort option", diff --git a/test/crud/unified/client-bulkWrite-updateOne-sort.json b/test/crud/unified/client-bulkWrite-updateOne-sort.json index 688bb01027..4a07b8b97c 100644 --- a/test/crud/unified/client-bulkWrite-updateOne-sort.json +++ b/test/crud/unified/client-bulkWrite-updateOne-sort.json @@ -1,6 +1,6 @@ { "description": "client bulkWrite updateOne-sort", - "schemaVersion": "1.0", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "8.0" @@ -51,6 +51,9 @@ ] } ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0" + }, "tests": [ { "description": "client bulkWrite updateOne with sort option", From 14c2d80850e292efc8f17ebf0652f31b10b91135 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 15 Oct 2024 21:17:25 -0500 Subject: [PATCH 7/8] address review --- doc/changelog.rst | 2 ++ pymongo/asynchronous/collection.py | 6 ++++-- pymongo/synchronous/collection.py | 6 ++++-- test/utils.py | 5 ----- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 3935fa3492..924d2ce40d 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -19,6 +19,8 @@ PyMongo 4.11 brings a number of changes including: until either connection succeeds or a server selection timeout error is raised. - Added :func:`repr` support to :class:`pymongo.operations.IndexModel`. - Added :func:`repr` support to :class:`pymongo.operations.SearchIndexModel`. +- Added ``sort`` parameter to + :meth:`~pymongo.collection.Collection.update_one` and :meth:`~pymongo.collection.Collection.replace_one` Issues Resolved ............... diff --git a/pymongo/asynchronous/collection.py b/pymongo/asynchronous/collection.py index 34e83731a0..9b73423627 100644 --- a/pymongo/asynchronous/collection.py +++ b/pymongo/asynchronous/collection.py @@ -1190,9 +1190,10 @@ async def replace_one( command. :param sort: Specify which document the operation updates if the query matches multiple documents. The first document matched by the sort order will be updated. + This option is only supported on MongoDB 8.0 and above. :return: - An instance of :class:`~pymongo.results.UpdateResult`. - .. versionchanged:: 4.10 + .. versionchanged:: 4.11 Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. @@ -1303,12 +1304,13 @@ async def update_one( aggregate expression context (e.g. "$$var"). :param sort: Specify which document the operation updates if the query matches multiple documents. The first document matched by the sort order will be updated. + This option is only supported on MongoDB 8.0 and above. :param comment: A user-provided comment to attach to this command. :return: - An instance of :class:`~pymongo.results.UpdateResult`. - .. versionchanged:: 4.10 + .. versionchanged:: 4.11 Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. diff --git a/pymongo/synchronous/collection.py b/pymongo/synchronous/collection.py index cc64a170ac..6edfddc9a9 100644 --- a/pymongo/synchronous/collection.py +++ b/pymongo/synchronous/collection.py @@ -1189,9 +1189,10 @@ def replace_one( command. :param sort: Specify which document the operation updates if the query matches multiple documents. The first document matched by the sort order will be updated. + This option is only supported on MongoDB 8.0 and above. :return: - An instance of :class:`~pymongo.results.UpdateResult`. - .. versionchanged:: 4.10 + .. versionchanged:: 4.11 Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. @@ -1302,12 +1303,13 @@ def update_one( aggregate expression context (e.g. "$$var"). :param sort: Specify which document the operation updates if the query matches multiple documents. The first document matched by the sort order will be updated. + This option is only supported on MongoDB 8.0 and above. :param comment: A user-provided comment to attach to this command. :return: - An instance of :class:`~pymongo.results.UpdateResult`. - .. versionchanged:: 4.10 + .. versionchanged:: 4.11 Added ``sort`` parameter. .. versionchanged:: 4.1 Added ``let`` parameter. diff --git a/test/utils.py b/test/utils.py index ae47bf1fe0..3eac4fa509 100644 --- a/test/utils.py +++ b/test/utils.py @@ -958,11 +958,6 @@ def parse_spec_options(opts): def prepare_spec_arguments(spec, arguments, opname, entity_map, with_txn_callback): for arg_name in list(arguments): c2s = camel_to_snake(arg_name) - # PyMongo accepts sort as list of tuples or as an optional dict for update/replaceOne. - if arg_name == "sort": - if spec["name"] not in ["replaceOne", "updateOne"]: - sort_dict = arguments[arg_name] - arguments[arg_name] = list(sort_dict.items()) # Named "key" instead not fieldName. if arg_name == "fieldName": arguments["key"] = arguments.pop(arg_name) From ac64d2cc0cee39bf849b9f54bffe4645af387967 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 16 Oct 2024 13:46:58 -0500 Subject: [PATCH 8/8] address review --- doc/changelog.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 924d2ce40d..4c1955d19d 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -20,7 +20,9 @@ PyMongo 4.11 brings a number of changes including: - Added :func:`repr` support to :class:`pymongo.operations.IndexModel`. - Added :func:`repr` support to :class:`pymongo.operations.SearchIndexModel`. - Added ``sort`` parameter to - :meth:`~pymongo.collection.Collection.update_one` and :meth:`~pymongo.collection.Collection.replace_one` + :meth:`~pymongo.collection.Collection.update_one`, :meth:`~pymongo.collection.Collection.replace_one`, + :class:`~pymongo.operations.UpdateOne`, and + :class:`~pymongo.operations.UpdateMany`, Issues Resolved ...............