PYTHON-1798 Support pipelines in update commands

This commit is contained in:
Prashant Mital 2019-06-13 19:59:20 -07:00
parent 694a4a5d85
commit aefd02a801
No known key found for this signature in database
GPG Key ID: 3D2DAA9E483ABE51
6 changed files with 290 additions and 12 deletions

View File

@ -51,6 +51,12 @@ Version 3.9 adds support for MongoDB 4.2. Highlights include:
:mod:`~pymongo.monitoring` for an example.
- :meth:`pymongo.collection.Collection.aggregate` and
:meth:`pymongo.database.Database.aggregate` now support the ``$merge`` pipeline
- Support for specifying a pipeline or document in
:meth:`~pymongo.collection.Collection.update_one`,
:meth:`~pymongo.collection.Collection.update_many`,
:meth:`~pymongo.collection.Collection.find_one_and_update`,
:meth:`~pymongo.operations.UpdateOne`, and
:meth:`~pymongo.operations.UpdateMany`.
.. _URI options specification: https://github.com/mongodb/specifications/blob/master/source/uri-options/uri-options.rst

View File

@ -974,6 +974,9 @@ class Collection(common.BaseObject):
.. note:: `bypass_document_validation` requires server version
**>= 3.2**
.. versionchanged:: 3.9
Added the ability to accept a pipeline as the `update`.
.. versionchanged:: 3.6
Added the `array_filters` and ``session`` parameters.
@ -1044,6 +1047,9 @@ class Collection(common.BaseObject):
.. note:: `bypass_document_validation` requires server version
**>= 3.2**
.. versionchanged:: 3.9
Added the ability to accept a pipeline as the `update`.
.. versionchanged:: 3.6
Added ``array_filters`` and ``session`` parameters.
@ -3087,6 +3093,8 @@ class Collection(common.BaseObject):
as keyword arguments (for example maxTimeMS can be used with
recent server versions).
.. versionchanged:: 3.9
Added the ability to accept a pipeline as the `update`.
.. versionchanged:: 3.6
Added the `array_filters` and `session` options.
.. versionchanged:: 3.4

View File

@ -457,11 +457,19 @@ def validate_list_or_none(option, value):
return validate_list(option, value)
def validate_list_or_mapping(option, value):
"""Validates that 'value' is a list or a document."""
if not isinstance(value, (abc.Mapping, list)):
raise TypeError("%s must either be a list or an instance of dict, "
"bson.son.SON, or any other type that inherits from "
"collections.Mapping" % (option,))
def validate_is_mapping(option, value):
"""Validate the type of method arguments that expect a document."""
if not isinstance(value, abc.Mapping):
raise TypeError("%s must be an instance of dict, bson.son.SON, or "
"other type that inherits from "
"any other type that inherits from "
"collections.Mapping" % (option,))
@ -515,12 +523,14 @@ def validate_ok_for_replace(replacement):
def validate_ok_for_update(update):
"""Validate an update document."""
validate_is_mapping("update", update)
# Update can not be {}
validate_list_or_mapping("update", update)
# Update cannot be {}.
if not update:
raise ValueError('update only works with $ operators')
raise ValueError('update cannot be empty')
is_document = not isinstance(update, list)
first = next(iter(update))
if not first.startswith('$'):
if is_document and not first.startswith('$'):
raise ValueError('update only works with $ operators')

View File

@ -243,6 +243,8 @@ class UpdateOne(_UpdateOp):
- `array_filters` (optional): A list of filters specifying which
array elements an update should apply. Requires MongoDB 3.6+.
.. versionchanged:: 3.9
Added the ability to accept a pipeline as the `update`.
.. versionchanged:: 3.6
Added the `array_filters` option.
.. versionchanged:: 3.5
@ -280,6 +282,8 @@ class UpdateMany(_UpdateOp):
- `array_filters` (optional): A list of filters specifying which
array elements an update should apply. Requires MongoDB 3.6+.
.. versionchanged:: 3.9
Added the ability to accept a pipeline as the `update`.
.. versionchanged:: 3.6
Added the `array_filters` option.
.. versionchanged:: 3.5

View File

@ -0,0 +1,239 @@
{
"data": [
{
"_id": 1,
"x": 1,
"y": 1,
"t": {
"u": {
"v": 1
}
}
},
{
"_id": 2,
"x": 2,
"y": 1
}
],
"minServerVersion": "4.1.11",
"collection_name": "test",
"database_name": "crud-tests",
"tests": [
{
"description": "UpdateOne using pipelines",
"operations": [
{
"name": "updateOne",
"arguments": {
"filter": {
"_id": 1
},
"update": [
{
"$replaceRoot": {
"newRoot": "$t"
}
},
{
"$addFields": {
"foo": 1
}
}
]
},
"result": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedCount": 0
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"update": "test",
"updates": [
{
"q": {
"_id": 1
},
"u": [
{
"$replaceRoot": {
"newRoot": "$t"
}
},
{
"$addFields": {
"foo": 1
}
}
]
}
]
},
"command_name": "update",
"database_name": "crud-tests"
}
}
],
"outcome": {
"collection": {
"data": [
{
"_id": 1,
"u": {
"v": 1
},
"foo": 1
},
{
"_id": 2,
"x": 2,
"y": 1
}
]
}
}
},
{
"description": "UpdateMany using pipelines",
"operations": [
{
"name": "updateMany",
"arguments": {
"filter": {},
"update": [
{
"$project": {
"x": 1
}
},
{
"$addFields": {
"foo": 1
}
}
]
},
"result": {
"matchedCount": 2,
"modifiedCount": 2,
"upsertedCount": 0
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"update": "test",
"updates": [
{
"q": {},
"u": [
{
"$project": {
"x": 1
}
},
{
"$addFields": {
"foo": 1
}
}
],
"multi": true
}
]
},
"command_name": "update",
"database_name": "crud-tests"
}
}
],
"outcome": {
"collection": {
"data": [
{
"_id": 1,
"x": 1,
"foo": 1
},
{
"_id": 2,
"x": 2,
"foo": 1
}
]
}
}
},
{
"description": "FindOneAndUpdate using pipelines",
"operations": [
{
"name": "findOneAndUpdate",
"arguments": {
"filter": {
"_id": 1
},
"update": [
{
"$project": {
"x": 1
}
},
{
"$addFields": {
"foo": 1
}
}
]
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"findAndModify": "test",
"update": [
{
"$project": {
"x": 1
}
},
{
"$addFields": {
"foo": 1
}
}
]
},
"command_name": "findAndModify",
"database_name": "crud-tests"
}
}
],
"outcome": {
"collection": {
"data": [
{
"_id": 1,
"x": 1,
"foo": 1
},
{
"_id": 2,
"x": 2,
"y": 1
}
]
}
}
}
]
}

View File

@ -139,7 +139,7 @@ class TestBulk(BulkTestBase):
self.assertEqual(1, result.inserted_count)
self.assertEqual(1, self.coll.count_documents({}))
def test_update_many(self):
def _test_update_many(self, update):
expected = {
'nMatched': 2,
@ -153,12 +153,18 @@ class TestBulk(BulkTestBase):
}
self.coll.insert_many([{}, {}])
result = self.coll.bulk_write([UpdateMany({},
{'$set': {'foo': 'bar'}})])
result = self.coll.bulk_write([UpdateMany({}, update)])
self.assertEqualResponse(expected, result.bulk_api_result)
self.assertEqual(2, result.matched_count)
self.assertTrue(result.modified_count in (2, None))
def test_update_many(self):
self._test_update_many({'$set': {'foo': 'bar'}})
@client_context.require_version_min(4, 1, 11)
def test_update_many_pipeline(self):
self._test_update_many([{'$set': {'foo': 'bar'}}])
@client_context.require_version_max(3, 5, 5)
def test_array_filters_unsupported(self):
requests = [
@ -184,8 +190,7 @@ class TestBulk(BulkTestBase):
self.assertRaises(ConfigurationError, coll.bulk_write, [update_one])
self.assertRaises(ConfigurationError, coll.bulk_write, [update_many])
def test_update_one(self):
def _test_update_one(self, update):
expected = {
'nMatched': 1,
'nModified': 1,
@ -199,12 +204,18 @@ class TestBulk(BulkTestBase):
self.coll.insert_many([{}, {}])
result = self.coll.bulk_write([UpdateOne({},
{'$set': {'foo': 'bar'}})])
result = self.coll.bulk_write([UpdateOne({}, update)])
self.assertEqualResponse(expected, result.bulk_api_result)
self.assertEqual(1, result.matched_count)
self.assertTrue(result.modified_count in (1, None))
def test_update_one(self):
self._test_update_one({'$set': {'foo': 'bar'}})
@client_context.require_version_min(4, 1, 11)
def test_update_one_pipeline(self):
self._test_update_one([{'$set': {'foo': 'bar'}}])
def test_replace_one(self):
expected = {