diff --git a/pymongo/bulk.py b/pymongo/bulk.py index cd942a488..41b2eedcd 100644 --- a/pymongo/bulk.py +++ b/pymongo/bulk.py @@ -156,6 +156,7 @@ class _Bulk(object): self.bypass_doc_val = bypass_document_validation self.uses_collation = False self.uses_array_filters = False + self.uses_hint = False self.is_retryable = True self.retrying = False self.started_retryable_write = False @@ -180,7 +181,7 @@ class _Bulk(object): self.ops.append((_INSERT, document)) def add_update(self, selector, update, multi=False, upsert=False, - collation=None, array_filters=None): + collation=None, array_filters=None, hint=None): """Create an update document and add it to the list of ops. """ validate_ok_for_update(update) @@ -193,13 +194,16 @@ class _Bulk(object): if array_filters is not None: self.uses_array_filters = True cmd['arrayFilters'] = array_filters + if hint is not None: + self.uses_hint = True + cmd['hint'] = hint if multi: # A bulk_write containing an update_many is not retryable. self.is_retryable = False self.ops.append((_UPDATE, cmd)) def add_replace(self, selector, replacement, upsert=False, - collation=None): + collation=None, hint=None): """Create a replace document and add it to the list of ops. """ validate_ok_for_replace(replacement) @@ -209,6 +213,9 @@ class _Bulk(object): if collation is not None: self.uses_collation = True cmd['collation'] = collation + if hint is not None: + self.uses_hint = True + cmd['hint'] = hint self.ops.append((_UPDATE, cmd)) def add_delete(self, selector, limit, collation=None): @@ -252,9 +259,13 @@ class _Bulk(object): def _execute_command(self, generator, write_concern, session, sock_info, op_id, retryable, full_result): - if sock_info.max_wire_version < 5 and self.uses_collation: - raise ConfigurationError( - 'Must be connected to MongoDB 3.4+ to use a collation.') + if sock_info.max_wire_version < 5: + if self.uses_collation: + raise ConfigurationError( + 'Must be connected to MongoDB 3.4+ to use a collation.') + if self.uses_hint: + raise ConfigurationError( + 'Must be connected to MongoDB 3.4+ to use hint.') if sock_info.max_wire_version < 6 and self.uses_array_filters: raise ConfigurationError( 'Must be connected to MongoDB 3.6+ to use arrayFilters.') @@ -428,6 +439,9 @@ class _Bulk(object): if self.uses_array_filters: raise ConfigurationError( 'arrayFilters is unsupported for unacknowledged writes.') + if self.uses_hint: + raise ConfigurationError( + 'hint is unsupported for unacknowledged writes.') # Cannot have both unacknowledged writes and bypass document validation. if self.bypass_doc_val and sock_info.max_wire_version >= 4: raise OperationFailure("Cannot set bypass_document_validation with" diff --git a/pymongo/collection.py b/pymongo/collection.py index 894d1a48a..502c9533d 100644 --- a/pymongo/collection.py +++ b/pymongo/collection.py @@ -761,7 +761,7 @@ class Collection(common.BaseObject): check_keys=True, multi=False, manipulate=False, write_concern=None, op_id=None, ordered=True, bypass_doc_val=False, collation=None, array_filters=None, - session=None, retryable_write=False): + hint=None, session=None, retryable_write=False): """Internal update / replace helper.""" common.validate_boolean("upsert", upsert) if manipulate: @@ -791,6 +791,17 @@ class Collection(common.BaseObject): 'arrayFilters is unsupported for unacknowledged writes.') else: update_doc['arrayFilters'] = array_filters + if hint is not None: + if sock_info.max_wire_version < 5: + raise ConfigurationError( + 'Must be connected to MongoDB 3.4+ to use hint.') + elif not acknowledged: + raise ConfigurationError( + 'hint is unsupported for unacknowledged writes.') + if not isinstance(hint, string_type): + hint = helpers._index_document(hint) + update_doc['hint'] = hint + command = SON([('update', self.name), ('ordered', ordered), ('updates', [update_doc])]) @@ -839,7 +850,7 @@ class Collection(common.BaseObject): check_keys=True, multi=False, manipulate=False, write_concern=None, op_id=None, ordered=True, bypass_doc_val=False, collation=None, array_filters=None, - session=None): + hint=None, session=None): """Internal update / replace helper.""" def _update(session, sock_info, retryable_write): return self._update( @@ -847,7 +858,7 @@ class Collection(common.BaseObject): check_keys=check_keys, multi=multi, manipulate=manipulate, write_concern=write_concern, op_id=op_id, ordered=ordered, bypass_doc_val=bypass_doc_val, collation=collation, - array_filters=array_filters, session=session, + array_filters=array_filters, hint=hint, session=session, retryable_write=retryable_write) return self.__database.client._retryable_write( @@ -856,7 +867,7 @@ class Collection(common.BaseObject): def replace_one(self, filter, replacement, upsert=False, bypass_document_validation=False, collation=None, - session=None): + hint=None, session=None): """Replace a single document matching the filter. >>> for doc in db.test.find({}): @@ -893,27 +904,30 @@ class Collection(common.BaseObject): match the filter. - `bypass_document_validation`: (optional) If ``True``, allows the write to opt-out of document level validation. Default is - ``False``. + ``False``. This option is only supported on MongoDB 3.2 and above. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. This option is only supported on MongoDB 3.4 and above. + - `hint` (optional): An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. - .. note:: `bypass_document_validation` requires server version - **>= 3.2** - + .. versionchanged:: 3.11 + Added ``hint`` parameter. .. versionchanged:: 3.6 Added ``session`` parameter. - .. versionchanged:: 3.4 Added the `collation` option. - .. versionchanged:: 3.2 - Added bypass_document_validation support + Added bypass_document_validation support. .. versionadded:: 3.0 """ @@ -926,12 +940,13 @@ class Collection(common.BaseObject): filter, replacement, upsert, write_concern=write_concern, bypass_doc_val=bypass_document_validation, - collation=collation, session=session), + collation=collation, hint=hint, session=session), write_concern.acknowledged) def update_one(self, filter, update, upsert=False, bypass_document_validation=False, - collation=None, array_filters=None, session=None): + collation=None, array_filters=None, hint=None, + session=None): """Update a single document matching the filter. >>> for doc in db.test.find(): @@ -959,32 +974,35 @@ class Collection(common.BaseObject): match the filter. - `bypass_document_validation`: (optional) If ``True``, allows the write to opt-out of document level validation. Default is - ``False``. + ``False``. This option is only supported on MongoDB 3.2 and above. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. This option is only supported on MongoDB 3.4 and above. - `array_filters` (optional): A list of filters specifying which - array elements an update should apply. Requires MongoDB 3.6+. + array elements an update should apply. This option is only + supported on MongoDB 3.6 and above. + - `hint` (optional): An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. - .. note:: `bypass_document_validation` requires server version - **>= 3.2** - + .. versionchanged:: 3.11 + Added ``hint`` parameter. .. versionchanged:: 3.9 - Added the ability to accept a pipeline as the `update`. - + Added the ability to accept a pipeline as the ``update``. .. versionchanged:: 3.6 - Added the `array_filters` and ``session`` parameters. - + Added the ``array_filters`` and ``session`` parameters. .. versionchanged:: 3.4 - Added the `collation` option. - + Added the ``collation`` option. .. versionchanged:: 3.2 - Added bypass_document_validation support + Added ``bypass_document_validation`` support. .. versionadded:: 3.0 """ @@ -999,12 +1017,12 @@ class Collection(common.BaseObject): write_concern=write_concern, bypass_doc_val=bypass_document_validation, collation=collation, array_filters=array_filters, - session=session), + hint=hint, session=session), write_concern.acknowledged) def update_many(self, filter, update, upsert=False, array_filters=None, bypass_document_validation=False, collation=None, - session=None): + hint=None, session=None): """Update one or more documents that match the filter. >>> for doc in db.test.find(): @@ -1032,32 +1050,35 @@ class Collection(common.BaseObject): match the filter. - `bypass_document_validation` (optional): If ``True``, allows the write to opt-out of document level validation. Default is - ``False``. + ``False``. This option is only supported on MongoDB 3.2 and above. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. This option is only supported on MongoDB 3.4 and above. - `array_filters` (optional): A list of filters specifying which - array elements an update should apply. Requires MongoDB 3.6+. + array elements an update should apply. This option is only + supported on MongoDB 3.6 and above. + - `hint` (optional): An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. - .. note:: `bypass_document_validation` requires server version - **>= 3.2** - + .. versionchanged:: 3.11 + Added ``hint`` parameter. .. versionchanged:: 3.9 Added the ability to accept a pipeline as the `update`. - .. versionchanged:: 3.6 Added ``array_filters`` and ``session`` parameters. - .. versionchanged:: 3.4 Added the `collation` option. - .. versionchanged:: 3.2 - Added bypass_document_validation support + Added bypass_document_validation support. .. versionadded:: 3.0 """ @@ -1072,7 +1093,7 @@ class Collection(common.BaseObject): write_concern=write_concern, bypass_doc_val=bypass_document_validation, collation=collation, array_filters=array_filters, - session=session), + hint=hint, session=session), write_concern.acknowledged) def drop(self, session=None): @@ -2834,7 +2855,8 @@ class Collection(common.BaseObject): def __find_and_modify(self, filter, projection, sort, upsert=None, return_document=ReturnDocument.BEFORE, - array_filters=None, session=None, **kwargs): + array_filters=None, hint=None, session=None, + **kwargs): """Internal findAndModify helper.""" common.validate_is_mapping("filter", filter) @@ -2854,6 +2876,9 @@ class Collection(common.BaseObject): if upsert is not None: common.validate_boolean("upsert", upsert) cmd["upsert"] = upsert + if hint is not None: + if not isinstance(hint, string_type): + hint = helpers._index_document(hint) write_concern = self._write_concern_for_cmd(cmd, session) @@ -2868,6 +2893,11 @@ class Collection(common.BaseObject): 'arrayFilters is unsupported for unacknowledged ' 'writes.') cmd["arrayFilters"] = array_filters + if hint is not None: + if sock_info.max_wire_version < 8: + raise ConfigurationError( + 'Must be connected to MongoDB 4.2+ to use hint.') + cmd['hint'] = hint if (sock_info.max_wire_version >= 4 and not write_concern.is_server_default): cmd['writeConcern'] = write_concern.document @@ -2952,7 +2982,7 @@ class Collection(common.BaseObject): def find_one_and_replace(self, filter, replacement, projection=None, sort=None, upsert=False, return_document=ReturnDocument.BEFORE, - session=None, **kwargs): + hint=None, session=None, **kwargs): """Finds a single document and replaces it, returning either the original or the replaced document. @@ -2994,16 +3024,24 @@ class Collection(common.BaseObject): if no document matches. If :attr:`ReturnDocument.AFTER`, returns the replaced or inserted document. + - `hint` (optional): An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. - `**kwargs` (optional): additional command arguments can be passed as keyword arguments (for example maxTimeMS can be used with recent server versions). + .. versionchanged:: 3.11 + Added the ``hint`` option. .. versionchanged:: 3.6 Added ``session`` parameter. .. versionchanged:: 3.4 - Added the `collation` option. + Added the ``collation`` option. .. versionchanged:: 3.2 Respects write concern. @@ -3019,12 +3057,13 @@ class Collection(common.BaseObject): kwargs['update'] = replacement return self.__find_and_modify(filter, projection, sort, upsert, return_document, - session=session, **kwargs) + 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, session=None, **kwargs): + array_filters=None, hint=None, session=None, + **kwargs): """Finds a single document and updates it, returning either the original or the updated document. @@ -3104,19 +3143,28 @@ class Collection(common.BaseObject): :attr:`ReturnDocument.AFTER`, returns the updated or inserted document. - `array_filters` (optional): A list of filters specifying which - array elements an update should apply. Requires MongoDB 3.6+. + array elements an update should apply. This option is only + supported on MongoDB 3.6 and above. + - `hint` (optional): An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.4 and above. - `session` (optional): a :class:`~pymongo.client_session.ClientSession`. - `**kwargs` (optional): additional command arguments can be passed as keyword arguments (for example maxTimeMS can be used with recent server versions). + .. versionchanged:: 3.11 + Added the ``hint`` option. .. versionchanged:: 3.9 - Added the ability to accept a pipeline as the `update`. + Added the ability to accept a pipeline as the ``update``. .. versionchanged:: 3.6 - Added the `array_filters` and `session` options. + Added the ``array_filters`` and ``session`` options. .. versionchanged:: 3.4 - Added the `collation` option. + Added the ``collation`` option. .. versionchanged:: 3.2 Respects write concern. @@ -3133,7 +3181,8 @@ class Collection(common.BaseObject): kwargs['update'] = update return self.__find_and_modify(filter, projection, sort, upsert, return_document, - array_filters, session=session, **kwargs) + array_filters, hint=hint, + session=session, **kwargs) def save(self, to_save, manipulate=True, check_keys=True, **kwargs): """Save a document in this collection. diff --git a/pymongo/operations.py b/pymongo/operations.py index 76974e75a..987a2cdfc 100644 --- a/pymongo/operations.py +++ b/pymongo/operations.py @@ -14,6 +14,9 @@ """Operation class definitions.""" +from bson.py3compat import string_type + +from pymongo import helpers from pymongo.common import validate_boolean, validate_is_mapping, validate_list from pymongo.collation import validate_collation_or_none from pymongo.helpers import _gen_index_name, _index_document, _index_list @@ -136,9 +139,10 @@ class DeleteMany(object): class ReplaceOne(object): """Represents a replace_one operation.""" - __slots__ = ("_filter", "_doc", "_upsert", "_collation") + __slots__ = ("_filter", "_doc", "_upsert", "_collation", "_hint") - def __init__(self, filter, replacement, upsert=False, collation=None): + def __init__(self, filter, replacement, upsert=False, collation=None, + hint=None): """Create a ReplaceOne instance. For use with :meth:`~pymongo.collection.Collection.bulk_write`. @@ -151,65 +155,43 @@ class ReplaceOne(object): - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. This option is only supported on MongoDB 3.4 and above. + - `hint` (optional): An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. + .. versionchanged:: 3.11 + Added the ``hint`` option. .. versionchanged:: 3.5 - Added the `collation` option. + Added the ``collation`` option. """ if filter is not None: validate_is_mapping("filter", filter) if upsert is not None: validate_boolean("upsert", upsert) + if hint is not None: + if not isinstance(hint, string_type): + hint = helpers._index_document(hint) + self._filter = filter self._doc = replacement self._upsert = upsert self._collation = collation + self._hint = hint def _add_to_bulk(self, bulkobj): """Add this operation to the _Bulk instance `bulkobj`.""" bulkobj.add_replace(self._filter, self._doc, self._upsert, - collation=self._collation) - - def __eq__(self, other): - if type(other) == type(self): - return ( - (other._filter, other._doc, other._upsert, other._collation) == - (self._filter, self._doc, self._upsert, self._collation)) - return NotImplemented - - def __ne__(self, other): - return not self == other - - def __repr__(self): - return "%s(%r, %r, %r, %r)" % ( - self.__class__.__name__, self._filter, self._doc, self._upsert, - self._collation) - - -class _UpdateOp(object): - """Private base class for update operations.""" - - __slots__ = ("_filter", "_doc", "_upsert", "_collation", "_array_filters") - - def __init__(self, filter, doc, upsert, collation, array_filters): - if filter is not None: - validate_is_mapping("filter", filter) - if upsert is not None: - validate_boolean("upsert", upsert) - if array_filters is not None: - validate_list("array_filters", array_filters) - self._filter = filter - self._doc = doc - self._upsert = upsert - self._collation = collation - self._array_filters = array_filters + collation=self._collation, hint=self._hint) def __eq__(self, other): if type(other) == type(self): return ( (other._filter, other._doc, other._upsert, other._collation, - other._array_filters) == - (self._filter, self._doc, self._upsert, self._collation, - self._array_filters)) + other._hint) == (self._filter, self._doc, self._upsert, + self._collation, other._hint)) return NotImplemented def __ne__(self, other): @@ -218,7 +200,50 @@ class _UpdateOp(object): def __repr__(self): return "%s(%r, %r, %r, %r, %r)" % ( self.__class__.__name__, self._filter, self._doc, self._upsert, - self._collation, self._array_filters) + self._collation, self._hint) + + +class _UpdateOp(object): + """Private base class for update operations.""" + + __slots__ = ("_filter", "_doc", "_upsert", "_collation", "_array_filters", + "_hint") + + def __init__(self, filter, doc, upsert, collation, array_filters, hint): + if filter is not None: + validate_is_mapping("filter", filter) + if upsert is not None: + validate_boolean("upsert", upsert) + if array_filters is not None: + validate_list("array_filters", array_filters) + if hint is not None: + if not isinstance(hint, string_type): + hint = helpers._index_document(hint) + + + self._filter = filter + self._doc = doc + self._upsert = upsert + self._collation = collation + self._array_filters = array_filters + self._hint = hint + + def __eq__(self, other): + if type(other) == type(self): + return ( + (other._filter, other._doc, other._upsert, other._collation, + other._array_filters, other._hint) == + (self._filter, self._doc, self._upsert, self._collation, + self._array_filters, self._hint)) + return NotImplemented + + def __ne__(self, other): + return not self == other + + def __repr__(self): + return "%s(%r, %r, %r, %r, %r, %r)" % ( + self.__class__.__name__, self._filter, self._doc, self._upsert, + self._collation, self._array_filters, self._hint) class UpdateOne(_UpdateOp): @@ -227,7 +252,7 @@ class UpdateOne(_UpdateOp): __slots__ = () def __init__(self, filter, update, upsert=False, collation=None, - array_filters=None): + array_filters=None, hint=None): """Represents an update_one operation. For use with :meth:`~pymongo.collection.Collection.bulk_write`. @@ -242,7 +267,15 @@ class UpdateOne(_UpdateOp): supported on MongoDB 3.4 and above. - `array_filters` (optional): A list of filters specifying which array elements an update should apply. Requires MongoDB 3.6+. + - `hint` (optional): An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. + .. versionchanged:: 3.11 + Added the `hint` option. .. versionchanged:: 3.9 Added the ability to accept a pipeline as the `update`. .. versionchanged:: 3.6 @@ -251,13 +284,14 @@ class UpdateOne(_UpdateOp): Added the `collation` option. """ super(UpdateOne, self).__init__(filter, update, upsert, collation, - array_filters) + array_filters, hint) def _add_to_bulk(self, bulkobj): """Add this operation to the _Bulk instance `bulkobj`.""" bulkobj.add_update(self._filter, self._doc, False, self._upsert, collation=self._collation, - array_filters=self._array_filters) + array_filters=self._array_filters, + hint=self._hint) class UpdateMany(_UpdateOp): @@ -266,7 +300,7 @@ class UpdateMany(_UpdateOp): __slots__ = () def __init__(self, filter, update, upsert=False, collation=None, - array_filters=None): + array_filters=None, hint=None): """Create an UpdateMany instance. For use with :meth:`~pymongo.collection.Collection.bulk_write`. @@ -281,7 +315,15 @@ class UpdateMany(_UpdateOp): supported on MongoDB 3.4 and above. - `array_filters` (optional): A list of filters specifying which array elements an update should apply. Requires MongoDB 3.6+. + - `hint` (optional): An index to use to support the query + predicate specified either by its string name, or in the same + format as passed to + :meth:`~pymongo.collection.Collection.create_index` (e.g. + ``[('field', ASCENDING)]``). This option is only supported on + MongoDB 4.2 and above. + .. versionchanged:: 3.11 + Added the `hint` option. .. versionchanged:: 3.9 Added the ability to accept a pipeline as the `update`. .. versionchanged:: 3.6 @@ -290,13 +332,14 @@ class UpdateMany(_UpdateOp): Added the `collation` option. """ super(UpdateMany, self).__init__(filter, update, upsert, collation, - array_filters) + array_filters, hint) def _add_to_bulk(self, bulkobj): """Add this operation to the _Bulk instance `bulkobj`.""" bulkobj.add_update(self._filter, self._doc, True, self._upsert, collation=self._collation, - array_filters=self._array_filters) + array_filters=self._array_filters, + hint=self._hint) class IndexModel(object): diff --git a/test/crud/v2/bulkWrite-update-hint.json b/test/crud/v2/bulkWrite-update-hint.json new file mode 100644 index 000000000..15e169f76 --- /dev/null +++ b/test/crud/v2/bulkWrite-update-hint.json @@ -0,0 +1,366 @@ +{ + "runOn": [ + { + "minServerVersion": "4.2.0" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ], + "collection_name": "test_bulkwrite_update_hint", + "tests": [ + { + "description": "BulkWrite updateOne with update hints", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + }, + { + "name": "updateOne", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "result": { + "deletedCount": 0, + "insertedCount": 0, + "insertedIds": {}, + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0, + "upsertedIds": {} + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_bulkwrite_update_hint", + "updates": [ + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + { + "q": { + "_id": 1 + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 13 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "BulkWrite updateMany with update hints", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + }, + { + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$lt": 3 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "result": { + "deletedCount": 0, + "insertedCount": 0, + "insertedIds": {}, + "matchedCount": 4, + "modifiedCount": 4, + "upsertedCount": 0, + "upsertedIds": {} + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_bulkwrite_update_hint", + "updates": [ + { + "q": { + "_id": { + "$lt": 3 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": "_id_" + }, + { + "q": { + "_id": { + "$lt": 3 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": { + "_id": 1 + } + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 13 + }, + { + "_id": 2, + "x": 24 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + } + }, + { + "description": "BulkWrite replaceOne with update hints", + "operations": [ + { + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 3 + }, + "replacement": { + "x": 333 + }, + "hint": "_id_" + } + }, + { + "name": "replaceOne", + "arguments": { + "filter": { + "_id": 4 + }, + "replacement": { + "x": 444 + }, + "hint": { + "_id": 1 + } + } + } + ], + "options": { + "ordered": true + } + }, + "result": { + "deletedCount": 0, + "insertedCount": 0, + "insertedIds": {}, + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0, + "upsertedIds": {} + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_bulkwrite_update_hint", + "updates": [ + { + "q": { + "_id": 3 + }, + "u": { + "x": 333 + }, + "hint": "_id_" + }, + { + "q": { + "_id": 4 + }, + "u": { + "x": 444 + }, + "hint": { + "_id": 1 + } + } + ], + "ordered": true + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 333 + }, + { + "_id": 4, + "x": 444 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/findOneAndReplace-hint.json b/test/crud/v2/findOneAndReplace-hint.json new file mode 100644 index 000000000..263fdf962 --- /dev/null +++ b/test/crud/v2/findOneAndReplace-hint.json @@ -0,0 +1,128 @@ +{ + "runOn": [ + { + "minServerVersion": "4.3.1" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndReplace_hint", + "tests": [ + { + "description": "FindOneAndReplace with hint string", + "operations": [ + { + "object": "collection", + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + }, + "hint": "_id_" + }, + "result": { + "_id": 1, + "x": 11 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndReplace_hint", + "query": { + "_id": 1 + }, + "update": { + "x": 33 + }, + "hint": "_id_" + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 33 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndReplace with hint document", + "operations": [ + { + "object": "collection", + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": 1 + }, + "replacement": { + "x": 33 + }, + "hint": { + "_id": 1 + } + }, + "result": { + "_id": 1, + "x": 11 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndReplace_hint", + "query": { + "_id": 1 + }, + "update": { + "x": 33 + }, + "hint": { + "_id": 1 + } + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 33 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/findOneAndUpdate-hint.json b/test/crud/v2/findOneAndUpdate-hint.json new file mode 100644 index 000000000..451eecc01 --- /dev/null +++ b/test/crud/v2/findOneAndUpdate-hint.json @@ -0,0 +1,136 @@ +{ + "runOn": [ + { + "minServerVersion": "4.3.1" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "findOneAndUpdate_hint", + "tests": [ + { + "description": "FindOneAndUpdate with hint string", + "operations": [ + { + "object": "collection", + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "result": { + "_id": 1, + "x": 11 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndUpdate_hint", + "query": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + }, + { + "description": "FindOneAndUpdate with hint document", + "operations": [ + { + "object": "collection", + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "result": { + "_id": 1, + "x": 11 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "findAndModify": "findOneAndUpdate_hint", + "query": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 22 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/replaceOne-hint.json b/test/crud/v2/replaceOne-hint.json new file mode 100644 index 000000000..de4aa4d02 --- /dev/null +++ b/test/crud/v2/replaceOne-hint.json @@ -0,0 +1,146 @@ +{ + "runOn": [ + { + "minServerVersion": "4.2.0" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "test_replaceone_hint", + "tests": [ + { + "description": "ReplaceOne with hint string", + "operations": [ + { + "object": "collection", + "name": "replaceOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "replacement": { + "x": 111 + }, + "hint": "_id_" + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_replaceone_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 111 + }, + "hint": "_id_" + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 111 + } + ] + } + } + }, + { + "description": "ReplaceOne with hint document", + "operations": [ + { + "object": "collection", + "name": "replaceOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "replacement": { + "x": 111 + }, + "hint": { + "_id": 1 + } + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_replaceone_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 111 + }, + "hint": { + "_id": 1 + } + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 111 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/updateMany-hint.json b/test/crud/v2/updateMany-hint.json new file mode 100644 index 000000000..489348917 --- /dev/null +++ b/test/crud/v2/updateMany-hint.json @@ -0,0 +1,168 @@ +{ + "runOn": [ + { + "minServerVersion": "4.2.0" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ], + "collection_name": "test_updatemany_hint", + "tests": [ + { + "description": "UpdateMany with hint string", + "operations": [ + { + "object": "collection", + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "result": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updatemany_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": "_id_" + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 23 + }, + { + "_id": 3, + "x": 34 + } + ] + } + } + }, + { + "description": "UpdateMany with hint document", + "operations": [ + { + "object": "collection", + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "result": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updatemany_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "hint": { + "_id": 1 + } + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 23 + }, + { + "_id": 3, + "x": 34 + } + ] + } + } + } + ] +} diff --git a/test/crud/v2/updateOne-hint.json b/test/crud/v2/updateOne-hint.json new file mode 100644 index 000000000..43f76da49 --- /dev/null +++ b/test/crud/v2/updateOne-hint.json @@ -0,0 +1,154 @@ +{ + "runOn": [ + { + "minServerVersion": "4.2.0" + } + ], + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ], + "collection_name": "test_updateone_hint", + "tests": [ + { + "description": "UpdateOne with hint string", + "operations": [ + { + "object": "collection", + "name": "updateOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updateone_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": "_id_" + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 23 + } + ] + } + } + }, + { + "description": "UpdateOne with hint document", + "operations": [ + { + "object": "collection", + "name": "updateOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + }, + "result": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectations": [ + { + "command_started_event": { + "command": { + "update": "test_updateone_hint", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "hint": { + "_id": 1 + } + } + ] + } + } + } + ], + "outcome": { + "collection": { + "data": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 23 + } + ] + } + } + } + ] +} diff --git a/test/utils_spec_runner.py b/test/utils_spec_runner.py index a200b41f9..4357b29d2 100644 --- a/test/utils_spec_runner.py +++ b/test/utils_spec_runner.py @@ -22,12 +22,13 @@ from bson import decode, encode from bson.binary import Binary, STANDARD from bson.codec_options import CodecOptions from bson.int64 import Int64 -from bson.py3compat import iteritems, abc, text_type +from bson.py3compat import iteritems, abc, string_type, text_type from bson.son import SON from gridfs import GridFSBucket from pymongo import (client_session, + helpers, operations) from pymongo.command_cursor import CommandCursor from pymongo.cursor import Cursor @@ -204,6 +205,25 @@ class SpecRunner(IntegrationTest): if 'maxCommitTimeMS' in opts: opts['max_commit_time_ms'] = opts.pop('maxCommitTimeMS') + if 'hint' in opts: + hint = opts.pop('hint') + if not isinstance(hint, string_type): + hint = list(iteritems(hint)) + opts['hint'] = hint + + # Properly format 'hint' arguments for the Bulk API tests. + if 'requests' in opts: + reqs = opts.pop('requests') + for req in reqs: + args = req.pop('arguments') + if 'hint' in args: + hint = args.pop('hint') + if not isinstance(hint, string_type): + hint = list(iteritems(hint)) + args['hint'] = hint + req['arguments'] = args + opts['requests'] = reqs + return dict(opts) def run_operation(self, sessions, collection, operation):