diff --git a/doc/changelog.rst b/doc/changelog.rst index 88c1b7cd20..192b456619 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,22 @@ Changelog ========= +Changes in Version 4.1 +---------------------- + +- :meth:`pymongo.collection.Collection.update_one`, + :meth:`pymongo.collection.Collection.update_many`, + :meth:`pymongo.collection.Collection.delete_one`, + :meth:`pymongo.collection.Collection.delete_many`, + :meth:`pymongo.collection.Collection.aggregate`, + :meth:`pymongo.collection.Collection.find_one_and_delete`, + :meth:`pymongo.collection.Collection.find_one_and_replace`, + :meth:`pymongo.collection.Collection.find_one_and_update`, + and :meth:`pymongo.collection.Collection.find` all support a new keyword + argument ``let`` which is a map of parameter names and values. Parameters + can then be accessed as variables in an aggregate expression context. + + Changes in Version 4.0 ---------------------- diff --git a/pymongo/aggregation.py b/pymongo/aggregation.py index 2a34a05d3a..4a565ee134 100644 --- a/pymongo/aggregation.py +++ b/pymongo/aggregation.py @@ -30,7 +30,7 @@ class _AggregationCommand(object): :meth:`pymongo.database.Database.aggregate` instead. """ def __init__(self, target, cursor_class, pipeline, options, - explicit_session, user_fields=None, result_processor=None): + explicit_session, let=None, user_fields=None, result_processor=None): if "explain" in options: raise ConfigurationError("The explain option is not supported. " "Use Database.command instead.") @@ -44,6 +44,9 @@ def __init__(self, target, cursor_class, pipeline, options, self._performs_write = True common.validate_is_mapping('options', options) + if let: + common.validate_is_mapping("let", let) + options["let"] = let self._options = options # This is the batchSize that will be used for setting the initial diff --git a/pymongo/collection.py b/pymongo/collection.py index 8632204b81..913441e5f0 100644 --- a/pymongo/collection.py +++ b/pymongo/collection.py @@ -586,7 +586,7 @@ def _update(self, sock_info, criteria, document, upsert=False, check_keys=False, multi=False, write_concern=None, op_id=None, ordered=True, bypass_doc_val=False, collation=None, array_filters=None, - hint=None, session=None, retryable_write=False): + hint=None, session=None, retryable_write=False, let=None): """Internal update / replace helper.""" common.validate_boolean("upsert", upsert) collation = validate_collation_or_none(collation) @@ -619,6 +619,9 @@ def _update(self, sock_info, criteria, document, upsert=False, command = SON([('update', self.name), ('ordered', ordered), ('updates', [update_doc])]) + if let: + common.validate_is_mapping("let", let) + command["let"] = let if not write_concern.is_server_default: command['writeConcern'] = write_concern.document @@ -656,7 +659,7 @@ def _update_retryable( check_keys=False, multi=False, write_concern=None, op_id=None, ordered=True, bypass_doc_val=False, collation=None, array_filters=None, - hint=None, session=None): + hint=None, session=None, let=None): """Internal update / replace helper.""" def _update(session, sock_info, retryable_write): return self._update( @@ -665,7 +668,7 @@ def _update(session, sock_info, retryable_write): write_concern=write_concern, op_id=op_id, ordered=ordered, bypass_doc_val=bypass_doc_val, collation=collation, array_filters=array_filters, hint=hint, session=session, - retryable_write=retryable_write) + retryable_write=retryable_write, let=let) return self.__database.client._retryable_write( (write_concern or self.write_concern).acknowledged and not multi, @@ -752,7 +755,7 @@ def replace_one(self, filter, replacement, upsert=False, def update_one(self, filter, update, upsert=False, bypass_document_validation=False, collation=None, array_filters=None, hint=None, - session=None): + session=None, let=None): """Update a single document matching the filter. >>> for doc in db.test.find(): @@ -795,10 +798,16 @@ def update_one(self, filter, update, upsert=False, MongoDB 4.2 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. + - `let` (optional): Map of parameter names and values. Values must be + 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"). :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. + .. versionchanged:: 4.1 + Added ``let`` parameter. .. versionchanged:: 3.11 Added ``hint`` parameter. .. versionchanged:: 3.9 @@ -823,12 +832,12 @@ def update_one(self, filter, update, upsert=False, write_concern=write_concern, bypass_doc_val=bypass_document_validation, collation=collation, array_filters=array_filters, - hint=hint, session=session), + hint=hint, session=session, let=let), write_concern.acknowledged) def update_many(self, filter, update, upsert=False, array_filters=None, bypass_document_validation=False, collation=None, - hint=None, session=None): + hint=None, session=None, let=None): """Update one or more documents that match the filter. >>> for doc in db.test.find(): @@ -871,10 +880,16 @@ def update_many(self, filter, update, upsert=False, array_filters=None, MongoDB 4.2 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. + - `let` (optional): Map of parameter names and values. Values must be + 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"). :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. + .. versionchanged:: 4.1 + Added ``let`` parameter. .. versionchanged:: 3.11 Added ``hint`` parameter. .. versionchanged:: 3.9 @@ -899,7 +914,7 @@ def update_many(self, filter, update, upsert=False, array_filters=None, write_concern=write_concern, bypass_doc_val=bypass_document_validation, collation=collation, array_filters=array_filters, - hint=hint, session=session), + hint=hint, session=session, let=let), write_concern.acknowledged) def drop(self, session=None): @@ -931,7 +946,8 @@ def drop(self, session=None): def _delete( self, sock_info, criteria, multi, write_concern=None, op_id=None, ordered=True, - collation=None, hint=None, session=None, retryable_write=False): + collation=None, hint=None, session=None, retryable_write=False, + let=None): """Internal delete helper.""" common.validate_is_mapping("filter", criteria) write_concern = write_concern or self.write_concern @@ -958,6 +974,10 @@ def _delete( if not write_concern.is_server_default: command['writeConcern'] = write_concern.document + if let: + common.validate_is_document_type("let", let) + command["let"] = let + # Delete command. result = sock_info.command( self.__database.name, @@ -973,20 +993,21 @@ def _delete( def _delete_retryable( self, criteria, multi, write_concern=None, op_id=None, ordered=True, - collation=None, hint=None, session=None): + collation=None, hint=None, session=None, let=None): """Internal delete helper.""" def _delete(session, sock_info, retryable_write): return self._delete( sock_info, criteria, multi, write_concern=write_concern, op_id=op_id, ordered=ordered, collation=collation, hint=hint, session=session, - retryable_write=retryable_write) + retryable_write=retryable_write, let=let) return self.__database.client._retryable_write( (write_concern or self.write_concern).acknowledged and not multi, _delete, session) - def delete_one(self, filter, collation=None, hint=None, session=None): + def delete_one(self, filter, collation=None, hint=None, session=None, + let=None): """Delete a single document matching the filter. >>> db.test.count_documents({'x': 1}) @@ -1010,10 +1031,16 @@ def delete_one(self, filter, collation=None, hint=None, session=None): MongoDB 4.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. + - `let` (optional): Map of parameter names and values. Values must be + 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"). :Returns: - An instance of :class:`~pymongo.results.DeleteResult`. + .. versionchanged:: 4.1 + Added ``let`` parameter. .. versionchanged:: 3.11 Added ``hint`` parameter. .. versionchanged:: 3.6 @@ -1027,10 +1054,11 @@ def delete_one(self, filter, collation=None, hint=None, session=None): self._delete_retryable( filter, False, write_concern=write_concern, - collation=collation, hint=hint, session=session), + collation=collation, hint=hint, session=session, let=let), write_concern.acknowledged) - def delete_many(self, filter, collation=None, hint=None, session=None): + def delete_many(self, filter, collation=None, hint=None, session=None, + let=None): """Delete one or more documents matching the filter. >>> db.test.count_documents({'x': 1}) @@ -1054,10 +1082,16 @@ def delete_many(self, filter, collation=None, hint=None, session=None): MongoDB 4.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. + - `let` (optional): Map of parameter names and values. Values must be + 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"). :Returns: - An instance of :class:`~pymongo.results.DeleteResult`. + .. versionchanged:: 4.1 + Added ``let`` parameter. .. versionchanged:: 3.11 Added ``hint`` parameter. .. versionchanged:: 3.6 @@ -1071,7 +1105,7 @@ def delete_many(self, filter, collation=None, hint=None, session=None): self._delete_retryable( filter, True, write_concern=write_concern, - collation=collation, hint=hint, session=session), + collation=collation, hint=hint, session=session, let=let), write_concern.acknowledged) def find_one(self, filter=None, *args, **kwargs): @@ -1882,15 +1916,16 @@ def options(self, session=None): return options def _aggregate(self, aggregation_command, pipeline, cursor_class, session, - explicit_session, **kwargs): + explicit_session, let=None, **kwargs): cmd = aggregation_command( - self, cursor_class, pipeline, kwargs, explicit_session, + self, cursor_class, pipeline, kwargs, explicit_session, let, user_fields={'cursor': {'firstBatch': 1}}) + return self.__database.client._retryable_read( cmd.get_cursor, cmd.get_read_preference(session), session, retryable=not cmd._performs_write) - def aggregate(self, pipeline, session=None, **kwargs): + def aggregate(self, pipeline, session=None, let=None, **kwargs): """Perform an aggregation using the aggregation framework on this collection. @@ -1937,6 +1972,8 @@ def aggregate(self, pipeline, session=None, **kwargs): A :class:`~pymongo.command_cursor.CommandCursor` over the result set. + .. versionchanged:: 4.1 + Added ``let`` parameter. .. versionchanged:: 4.0 Removed the ``useCursor`` option. .. versionchanged:: 3.9 @@ -1966,6 +2003,7 @@ def aggregate(self, pipeline, session=None, **kwargs): CommandCursor, session=s, explicit_session=session is not None, + let=let, **kwargs) def aggregate_raw_batches(self, pipeline, session=None, **kwargs): @@ -2225,7 +2263,7 @@ def _write_concern_for_cmd(self, cmd, session): def __find_and_modify(self, filter, projection, sort, upsert=None, return_document=ReturnDocument.BEFORE, array_filters=None, hint=None, session=None, - **kwargs): + let=None, **kwargs): """Internal findAndModify helper.""" common.validate_is_mapping("filter", filter) @@ -2236,6 +2274,9 @@ def __find_and_modify(self, filter, projection, sort, upsert=None, cmd = SON([("findAndModify", self.__name), ("query", filter), ("new", return_document)]) + if let: + common.validate_is_mapping("let", let) + cmd["let"] = let cmd.update(kwargs) if projection is not None: cmd["fields"] = helpers._fields_list_to_dict(projection, @@ -2283,7 +2324,7 @@ def _find_and_modify(session, sock_info, retryable_write): def find_one_and_delete(self, filter, projection=None, sort=None, hint=None, - session=None, **kwargs): + session=None, let=None, **kwargs): """Finds a single document and deletes it, returning the document. >>> db.test.count_documents({'x': 1}) @@ -2330,7 +2371,13 @@ def find_one_and_delete(self, filter, - `**kwargs` (optional): additional command arguments can be passed as keyword arguments (for example maxTimeMS can be used with recent server versions). + - `let` (optional): Map of parameter names and values. Values must be + 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"). + .. versionchanged:: 4.1 + Added ``let`` parameter. .. versionchanged:: 3.11 Added ``hint`` parameter. .. versionchanged:: 3.6 @@ -2349,13 +2396,13 @@ def find_one_and_delete(self, filter, .. versionadded:: 3.0 """ kwargs['remove'] = True - return self.__find_and_modify(filter, projection, sort, + return self.__find_and_modify(filter, projection, sort, let=let, hint=hint, session=session, **kwargs) def find_one_and_replace(self, filter, replacement, projection=None, sort=None, upsert=False, return_document=ReturnDocument.BEFORE, - hint=None, session=None, **kwargs): + hint=None, session=None, let=None, **kwargs): """Finds a single document and replaces it, returning either the original or the replaced document. @@ -2405,10 +2452,16 @@ def find_one_and_replace(self, filter, replacement, MongoDB 4.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. + - `let` (optional): Map of parameter names and values. Values must be + 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"). - `**kwargs` (optional): additional command arguments can be passed as keyword arguments (for example maxTimeMS can be used with recent server versions). + .. versionchanged:: 4.1 + Added ``let`` parameter. .. versionchanged:: 3.11 Added the ``hint`` option. .. versionchanged:: 3.6 @@ -2429,14 +2482,14 @@ def find_one_and_replace(self, filter, replacement, common.validate_ok_for_replace(replacement) kwargs['update'] = replacement return self.__find_and_modify(filter, projection, - sort, upsert, return_document, + sort, upsert, return_document, let=let, hint=hint, session=session, **kwargs) def find_one_and_update(self, filter, update, projection=None, sort=None, upsert=False, return_document=ReturnDocument.BEFORE, array_filters=None, hint=None, session=None, - **kwargs): + let=None, **kwargs): """Finds a single document and updates it, returning either the original or the updated document. @@ -2526,10 +2579,16 @@ def find_one_and_update(self, filter, update, MongoDB 4.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. + - `let` (optional): Map of parameter names and values. Values must be + 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"). - `**kwargs` (optional): additional command arguments can be passed as keyword arguments (for example maxTimeMS can be used with recent server versions). + .. versionchanged:: 4.1 + Added ``let`` parameter. .. versionchanged:: 3.11 Added the ``hint`` option. .. versionchanged:: 3.9 @@ -2554,7 +2613,7 @@ def find_one_and_update(self, filter, update, kwargs['update'] = update return self.__find_and_modify(filter, projection, sort, upsert, return_document, - array_filters, hint=hint, + array_filters, hint=hint, let=let, session=session, **kwargs) def __iter__(self): diff --git a/pymongo/cursor.py b/pymongo/cursor.py index c38adaf377..e825edf8fd 100644 --- a/pymongo/cursor.py +++ b/pymongo/cursor.py @@ -24,7 +24,8 @@ from bson.code import Code from bson.son import SON from pymongo import helpers -from pymongo.common import validate_boolean, validate_is_mapping +from pymongo.common import (validate_boolean, validate_is_mapping, + validate_is_document_type) from pymongo.collation import validate_collation_or_none from pymongo.errors import (ConnectionFailure, InvalidOperation, @@ -140,7 +141,7 @@ def __init__(self, collection, filter=None, projection=None, skip=0, collation=None, hint=None, max_scan=None, max_time_ms=None, max=None, min=None, return_key=None, show_record_id=None, snapshot=None, comment=None, session=None, - allow_disk_use=None): + allow_disk_use=None, let=None): """Create a new cursor. Should not be called directly by application developers - see @@ -197,6 +198,10 @@ def __init__(self, collection, filter=None, projection=None, skip=0, if projection is not None: projection = helpers._fields_list_to_dict(projection, "projection") + if let: + validate_is_document_type("let", let) + + self.__let = let self.__spec = spec self.__projection = projection self.__skip = skip @@ -370,6 +375,8 @@ def __query_spec(self): operators["$explain"] = True if self.__hint: operators["$hint"] = self.__hint + if self.__let: + operators["let"] = self.__let if self.__comment: operators["$comment"] = self.__comment if self.__max_scan: diff --git a/test/crud/unified/aggregate-let.json b/test/crud/unified/aggregate-let.json index d3b76bd65a..039900920f 100644 --- a/test/crud/unified/aggregate-let.json +++ b/test/crud/unified/aggregate-let.json @@ -56,109 +56,6 @@ "minServerVersion": "5.0" } ], - "operations": [ - { - "name": "aggregate", - "object": "collection0", - "arguments": { - "pipeline": [ - { - "$match": { - "$expr": { - "$eq": [ - "$_id", - "$$id" - ] - } - } - }, - { - "$project": { - "_id": 0, - "x": "$$x", - "y": "$$y", - "rand": "$$rand" - } - } - ], - "let": { - "id": 1, - "x": "foo", - "y": { - "$literal": "bar" - }, - "rand": { - "$rand": {} - } - } - }, - "expectResult": [ - { - "x": "foo", - "y": "bar", - "rand": { - "$$type": "double" - } - } - ] - } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "aggregate": "coll0", - "pipeline": [ - { - "$match": { - "$expr": { - "$eq": [ - "$_id", - "$$id" - ] - } - } - }, - { - "$project": { - "_id": 0, - "x": "$$x", - "y": "$$y", - "rand": "$$rand" - } - } - ], - "let": { - "id": 1, - "x": "foo", - "y": { - "$literal": "bar" - }, - "rand": { - "$rand": {} - } - } - } - } - } - ] - } - ] - }, - { - "description": "Aggregate with let option and dollar-prefixed $literal value", - "runOnRequirements": [ - { - "minServerVersion": "5.0", - "topologies": [ - "single", - "replicaset" - ] - } - ], "operations": [ { "name": "aggregate", diff --git a/test/crud/unified/deleteMany-let.json b/test/crud/unified/deleteMany-let.json new file mode 100644 index 0000000000..71bf26a013 --- /dev/null +++ b/test/crud/unified/deleteMany-let.json @@ -0,0 +1,201 @@ +{ + "description": "deleteMany-let", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2, + "name": "name" + }, + { + "_id": 3, + "name": "name" + } + ] + } + ], + "tests": [ + { + "description": "deleteMany with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "deleteMany", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$name", + "$$name" + ] + } + }, + "let": { + "name": "name" + } + }, + "expectResult": { + "deletedCount": 2 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "coll0", + "deletes": [ + { + "q": { + "$expr": { + "$eq": [ + "$name", + "$$name" + ] + } + }, + "limit": 0 + } + ], + "let": { + "name": "name" + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + } + ] + } + ] + }, + { + "description": "deleteMany with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "3.6.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "deleteMany", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$name", + "$$name" + ] + } + }, + "let": { + "name": "name" + } + }, + "expectError": { + "errorContains": "'delete.let' is an unknown field", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "coll0", + "deletes": [ + { + "q": { + "$expr": { + "$eq": [ + "$name", + "$$name" + ] + } + }, + "limit": 0 + } + ], + "let": { + "name": "name" + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2, + "name": "name" + }, + { + "_id": 3, + "name": "name" + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/deleteOne-let.json b/test/crud/unified/deleteOne-let.json new file mode 100644 index 0000000000..9718682235 --- /dev/null +++ b/test/crud/unified/deleteOne-let.json @@ -0,0 +1,191 @@ +{ + "description": "deleteOne-let", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "tests": [ + { + "description": "deleteOne with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "deleteOne", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "let": { + "id": 1 + } + }, + "expectResult": { + "deletedCount": 1 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "coll0", + "deletes": [ + { + "q": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "limit": 1 + } + ], + "let": { + "id": 1 + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 2 + } + ] + } + ] + }, + { + "description": "deleteOne with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "3.6.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "deleteOne", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "let": { + "id": 1 + } + }, + "expectError": { + "errorContains": "'delete.let' is an unknown field", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "coll0", + "deletes": [ + { + "q": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "limit": 1 + } + ], + "let": { + "id": 1 + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/find-let.json b/test/crud/unified/find-let.json new file mode 100644 index 0000000000..4e9c9c99f4 --- /dev/null +++ b/test/crud/unified/find-let.json @@ -0,0 +1,148 @@ +{ + "description": "find-let", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "tests": [ + { + "description": "Find with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "let": { + "id": 1 + } + }, + "expectResult": [ + { + "_id": 1 + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "coll0", + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "let": { + "id": 1 + } + } + } + } + ] + } + ] + }, + { + "description": "Find with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "3.6.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "let": { + "x": 1 + } + }, + "expectError": { + "errorContains": "Unrecognized field 'let'", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "coll0", + "filter": { + "_id": 1 + }, + "let": { + "x": 1 + } + } + } + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/findOneAndDelete-let.json b/test/crud/unified/findOneAndDelete-let.json new file mode 100644 index 0000000000..ba8e681c0e --- /dev/null +++ b/test/crud/unified/findOneAndDelete-let.json @@ -0,0 +1,180 @@ +{ + "description": "findOneAndDelete-let", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "tests": [ + { + "description": "findOneAndDelete with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "findOneAndDelete", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "let": { + "id": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "coll0", + "query": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "remove": true, + "let": { + "id": 1 + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 2 + } + ] + } + ] + }, + { + "description": "findOneAndDelete with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "4.2.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "findOneAndDelete", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "let": { + "id": 1 + } + }, + "expectError": { + "errorContains": "field 'let' is an unknown field", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "coll0", + "query": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "remove": true, + "let": { + "id": 1 + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/findOneAndReplace-let.json b/test/crud/unified/findOneAndReplace-let.json new file mode 100644 index 0000000000..5e5de44b31 --- /dev/null +++ b/test/crud/unified/findOneAndReplace-let.json @@ -0,0 +1,197 @@ +{ + "description": "findOneAndReplace-let", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "tests": [ + { + "description": "findOneAndReplace with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "findOneAndReplace", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "replacement": { + "x": "x" + }, + "let": { + "id": 1 + } + }, + "expectResult": { + "_id": 1 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "coll0", + "query": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "update": { + "x": "x" + }, + "let": { + "id": 1 + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": "x" + }, + { + "_id": 2 + } + ] + } + ] + }, + { + "description": "findOneAndReplace with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "4.2.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "findOneAndReplace", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "replacement": { + "x": "x" + }, + "let": { + "id": 1 + } + }, + "expectError": { + "errorContains": "field 'let' is an unknown field", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "coll0", + "query": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "update": { + "x": "x" + }, + "let": { + "id": 1 + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/findOneAndUpdate-let.json b/test/crud/unified/findOneAndUpdate-let.json new file mode 100644 index 0000000000..74d7d0e58b --- /dev/null +++ b/test/crud/unified/findOneAndUpdate-let.json @@ -0,0 +1,217 @@ +{ + "description": "findOneAndUpdate-let", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "tests": [ + { + "description": "findOneAndUpdate with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "update": [ + { + "$set": { + "x": "$$x" + } + } + ], + "let": { + "id": 1, + "x": "foo" + } + }, + "expectResult": { + "_id": 1 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "coll0", + "query": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "update": [ + { + "$set": { + "x": "$$x" + } + } + ], + "let": { + "id": 1, + "x": "foo" + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": "foo" + }, + { + "_id": 2 + } + ] + } + ] + }, + { + "description": "findOneAndUpdate with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "4.2.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "update": [ + { + "$set": { + "x": "$$x" + } + } + ], + "let": { + "id": 1, + "x": "foo" + } + }, + "expectError": { + "errorContains": "field 'let' is an unknown field", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "coll0", + "query": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "update": [ + { + "$set": { + "x": "$$x" + } + } + ], + "let": { + "id": 1, + "x": "foo" + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/updateMany-let.json b/test/crud/unified/updateMany-let.json new file mode 100644 index 0000000000..b4a4ddd800 --- /dev/null +++ b/test/crud/unified/updateMany-let.json @@ -0,0 +1,243 @@ +{ + "description": "updateMany-let", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2, + "name": "name" + }, + { + "_id": 3, + "name": "name" + } + ] + } + ], + "tests": [ + { + "description": "updateMany with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "updateMany", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$name", + "$$name" + ] + } + }, + "update": [ + { + "$set": { + "x": "$$x", + "y": "$$y" + } + } + ], + "let": { + "name": "name", + "x": "foo", + "y": { + "$literal": "bar" + } + } + }, + "expectResult": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "$expr": { + "$eq": [ + "$name", + "$$name" + ] + } + }, + "u": [ + { + "$set": { + "x": "$$x", + "y": "$$y" + } + } + ], + "multi": true + } + ], + "let": { + "name": "name", + "x": "foo", + "y": { + "$literal": "bar" + } + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2, + "name": "name", + "x": "foo", + "y": "bar" + }, + { + "_id": 3, + "name": "name", + "x": "foo", + "y": "bar" + } + ] + } + ] + }, + { + "description": "updateMany with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "3.6.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "updateMany", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": [ + { + "$set": { + "x": "$$x" + } + } + ], + "let": { + "x": "foo" + } + }, + "expectError": { + "errorContains": "'update.let' is an unknown field", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": [ + { + "$set": { + "x": "$$x" + } + } + ], + "multi": true + } + ], + "let": { + "x": "foo" + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2, + "name": "name" + }, + { + "_id": 3, + "name": "name" + } + ] + } + ] + } + ] +} diff --git a/test/crud/unified/updateOne-let.json b/test/crud/unified/updateOne-let.json new file mode 100644 index 0000000000..7b1cc4cf00 --- /dev/null +++ b/test/crud/unified/updateOne-let.json @@ -0,0 +1,215 @@ +{ + "description": "updateOne-let", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "tests": [ + { + "description": "UpdateOne with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "update": [ + { + "$set": { + "x": "$$x" + } + } + ], + "let": { + "id": 1, + "x": "foo" + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "u": [ + { + "$set": { + "x": "$$x" + } + } + ] + } + ], + "let": { + "id": 1, + "x": "foo" + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": "foo" + }, + { + "_id": 2 + } + ] + } + ] + }, + { + "description": "UpdateOne with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "3.6.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": [ + { + "$set": { + "x": "$$x" + } + } + ], + "let": { + "x": "foo" + } + }, + "expectError": { + "errorContains": "'update.let' is an unknown field", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": [ + { + "$set": { + "x": "$$x" + } + } + ] + } + ], + "let": { + "x": "foo" + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ] + } + ] +} diff --git a/test/test_collection.py b/test/test_collection.py index 79a2a907a6..4af2298ceb 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -2178,6 +2178,23 @@ def test_bool(self): with self.assertRaises(NotImplementedError): bool(Collection(self.db, 'test')) + @client_context.require_version_min(5, 0, 0) + def test_helpers_with_let(self): + c = self.db.test + helpers = [(c.delete_many, ({}, {})), (c.delete_one, ({}, {})), + (c.find, ({})), (c.update_many, ({}, {'$inc': {'x': 3}})), + (c.update_one, ({}, {'$inc': {'x': 3}})), + (c.find_one_and_delete, ({}, {})), + (c.find_one_and_replace, ({}, {})), + (c.aggregate, ([], {}))] + for let in [10, "str"]: + for helper, args in helpers: + with self.assertRaisesRegex(TypeError, + "let must be an instance of dict"): + helper(*args, let=let) + for helper, args in helpers: + helper(*args, let={}) + if __name__ == "__main__": unittest.main()