diff --git a/doc/changelog.rst b/doc/changelog.rst index 88c1b7cd2..192b45661 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 2a34a05d3..4a565ee13 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 @@ class _AggregationCommand(object): 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 774c29023..393c26aa5 100644 --- a/pymongo/collection.py +++ b/pymongo/collection.py @@ -593,7 +593,7 @@ class Collection(common.BaseObject): 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) @@ -626,6 +626,9 @@ class Collection(common.BaseObject): 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 @@ -663,7 +666,7 @@ class Collection(common.BaseObject): 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( @@ -672,7 +675,7 @@ class Collection(common.BaseObject): 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, @@ -759,7 +762,7 @@ class Collection(common.BaseObject): 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(): @@ -802,10 +805,16 @@ class Collection(common.BaseObject): 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 @@ -830,12 +839,12 @@ class Collection(common.BaseObject): 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(): @@ -878,10 +887,16 @@ class Collection(common.BaseObject): 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 @@ -906,7 +921,7 @@ class Collection(common.BaseObject): 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): @@ -938,7 +953,8 @@ class Collection(common.BaseObject): 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 @@ -965,6 +981,10 @@ class Collection(common.BaseObject): 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, @@ -980,20 +1000,21 @@ class Collection(common.BaseObject): 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}) @@ -1017,10 +1038,16 @@ class Collection(common.BaseObject): 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 @@ -1034,10 +1061,11 @@ class Collection(common.BaseObject): 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}) @@ -1061,10 +1089,16 @@ class Collection(common.BaseObject): 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 @@ -1078,7 +1112,7 @@ class Collection(common.BaseObject): 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): @@ -1889,15 +1923,16 @@ class Collection(common.BaseObject): 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. @@ -1944,6 +1979,8 @@ class Collection(common.BaseObject): 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 @@ -1973,6 +2010,7 @@ class Collection(common.BaseObject): CommandCursor, session=s, explicit_session=session is not None, + let=let, **kwargs) def aggregate_raw_batches(self, pipeline, session=None, **kwargs): @@ -2232,7 +2270,7 @@ class Collection(common.BaseObject): 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) @@ -2243,6 +2281,9 @@ class Collection(common.BaseObject): 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, @@ -2290,7 +2331,7 @@ class Collection(common.BaseObject): 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}) @@ -2337,7 +2378,13 @@ class Collection(common.BaseObject): - `**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 @@ -2356,13 +2403,13 @@ class Collection(common.BaseObject): .. 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. @@ -2412,10 +2459,16 @@ class Collection(common.BaseObject): 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 @@ -2436,14 +2489,14 @@ class Collection(common.BaseObject): 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. @@ -2533,10 +2586,16 @@ class Collection(common.BaseObject): 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 @@ -2561,7 +2620,7 @@ class Collection(common.BaseObject): 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 c38adaf37..e825edf8f 100644 --- a/pymongo/cursor.py +++ b/pymongo/cursor.py @@ -24,7 +24,8 @@ from bson import RE_TYPE, _convert_raw_document_lists_to_streams 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 @@ class Cursor(object): 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 @@ class Cursor(object): 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 @@ class Cursor(object): 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 d3b76bd65..039900920 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 000000000..71bf26a01 --- /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 000000000..971868223 --- /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 000000000..4e9c9c99f --- /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 000000000..ba8e681c0 --- /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 000000000..5e5de44b3 --- /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 000000000..74d7d0e58 --- /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 000000000..b4a4ddd80 --- /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 000000000..7b1cc4cf0 --- /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 79a2a907a..4af2298ce 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -2178,6 +2178,23 @@ class TestCollection(IntegrationTest): 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()