Skip to content

Commit be0ad8d

Browse files
committed
PYTHON-821 - Implement Collection.bulk_write.
1 parent a2c1309 commit be0ad8d

File tree

4 files changed

+440
-133
lines changed

4 files changed

+440
-133
lines changed

doc/api/pymongo/collection.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
.. automethod:: remove([spec_or_id=None[, multi=True[, **kwargs]]])
4040
.. automethod:: initialize_unordered_bulk_op
4141
.. automethod:: initialize_ordered_bulk_op
42+
.. automethod:: bulk_write
4243
.. automethod:: drop
4344
.. automethod:: find([filter=None[, projection=None[, skip=0[, limit=0[, no_cursor_timeout=False[, cursor_type=NON_TAILABLE[, sort=None[, allow_partial_results=False[, oplog_replay=False[, modifiers=None[, manipulate=True]]]]]]]]]]])
4445
.. automethod:: find_one([filter_or_id=None[, *args[, **kwargs]]])

pymongo/bulk.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,243 @@
4242
_COMMANDS = ('insert', 'update', 'delete')
4343

4444

45+
class _WriteOp(object):
46+
"""Private base class for all write operations."""
47+
48+
__slots__ = ("_filter", "_doc", "_upsert")
49+
50+
def __init__(self, filter=None, doc=None, upsert=None):
51+
if filter is not None and not isinstance(filter, collections.Mapping):
52+
raise TypeError("filter must be a mapping type.")
53+
if upsert is not None and not isinstance(upsert, bool):
54+
raise TypeError("upsert must be True or False")
55+
self._filter = filter
56+
self._doc = doc
57+
self._upsert = upsert
58+
59+
60+
class InsertOne(_WriteOp):
61+
"""Represents an insert_one operation."""
62+
63+
def __init__(self, document):
64+
"""Create an InsertOne instance.
65+
66+
For use with :meth:`~pymongo.collection.Collection.bulk_write`.
67+
68+
:Parameters:
69+
- `document`: The document to insert. If the document is missing an
70+
_id field one will be added.
71+
"""
72+
super(InsertOne, self).__init__(doc=document)
73+
74+
def _add_to_bulk(self, bulkobj):
75+
"""Add this operation to the _Bulk instance `bulkobj`."""
76+
bulkobj.add_insert(self._doc)
77+
78+
def __repr__(self):
79+
return "InsertOne(%r)" % (self._doc,)
80+
81+
82+
class DeleteOne(_WriteOp):
83+
"""Represents a delete_one operation."""
84+
85+
def __init__(self, filter):
86+
"""Create a DeleteOne instance.
87+
88+
For use with :meth:`~pymongo.collection.Collection.bulk_write`.
89+
90+
:Parameters:
91+
- `filter`: A query that matches the document to delete.
92+
"""
93+
super(DeleteOne, self).__init__(filter)
94+
95+
def _add_to_bulk(self, bulkobj):
96+
"""Add this operation to the _Bulk instance `bulkobj`."""
97+
bulkobj.add_delete(self._filter, 1)
98+
99+
def __repr__(self):
100+
return "DeleteOne(%r)" % (self._filter,)
101+
102+
103+
class DeleteMany(_WriteOp):
104+
"""Represents a delete_many operation."""
105+
106+
def __init__(self, filter):
107+
"""Create a DeleteMany instance.
108+
109+
For use with :meth:`~pymongo.collection.Collection.bulk_write`.
110+
111+
:Parameters:
112+
- `filter`: A query that matches the documents to delete.
113+
"""
114+
super(DeleteMany, self).__init__(filter)
115+
116+
def _add_to_bulk(self, bulkobj):
117+
"""Add this operation to the _Bulk instance `bulkobj`."""
118+
bulkobj.add_delete(self._filter, 0)
119+
120+
def __repr__(self):
121+
return "DeleteMany(%r)" % (self._filter,)
122+
123+
124+
class ReplaceOne(_WriteOp):
125+
"""Represents a replace_one operation."""
126+
127+
def __init__(self, filter, replacement, upsert=False):
128+
"""Create a ReplaceOne instance.
129+
130+
For use with :meth:`~pymongo.collection.Collection.bulk_write`.
131+
132+
:Parameters:
133+
- `filter`: A query that matches the document to replace.
134+
- `replacement`: The new document.
135+
- `upsert` (optional): If ``True``, perform an insert if no documents
136+
match the filter.
137+
"""
138+
super(ReplaceOne, self).__init__(filter, replacement, upsert)
139+
140+
def _add_to_bulk(self, bulkobj):
141+
"""Add this operation to the _Bulk instance `bulkobj`."""
142+
bulkobj.add_replace(self._filter, self._doc, self._upsert)
143+
144+
def __repr__(self):
145+
return "ReplaceOne(%r, %r, %r)" % (self._filter,
146+
self._doc,
147+
self._upsert)
148+
149+
150+
class UpdateOne(_WriteOp):
151+
"""Represents an update_one operation."""
152+
153+
def __init__(self, filter, update, upsert=False):
154+
"""Represents an update_one operation.
155+
156+
For use with :meth:`~pymongo.collection.Collection.bulk_write`.
157+
158+
:Parameters:
159+
- `filter`: A query that matches the document to update.
160+
- `update`: The modifications to apply.
161+
- `upsert` (optional): If ``True``, perform an insert if no documents
162+
match the filter.
163+
"""
164+
super(UpdateOne, self).__init__(filter, update, upsert)
165+
166+
def _add_to_bulk(self, bulkobj):
167+
"""Add this operation to the _Bulk instance `bulkobj`."""
168+
bulkobj.add_update(self._filter, self._doc, False, self._upsert)
169+
170+
def __repr__(self):
171+
return "UpdateOne(%r, %r, %r)" % (self._filter,
172+
self._doc,
173+
self._upsert)
174+
175+
176+
class UpdateMany(_WriteOp):
177+
"""Represents an update_many operation."""
178+
179+
def __init__(self, filter, update, upsert=False):
180+
"""Create an UpdateMany instance.
181+
182+
For use with :meth:`~pymongo.collection.Collection.bulk_write`.
183+
184+
:Parameters:
185+
- `filter`: A query that matches the documents to update.
186+
- `update`: The modifications to apply.
187+
- `upsert` (optional): If ``True``, perform an insert if no documents
188+
match the filter.
189+
"""
190+
super(UpdateMany, self).__init__(filter, update, upsert)
191+
192+
def _add_to_bulk(self, bulkobj):
193+
"""Add this operation to the _Bulk instance `bulkobj`."""
194+
bulkobj.add_update(self._filter, self._doc, True, self._upsert)
195+
196+
def __repr__(self):
197+
return "UpdateMany(%r, %r, %r)" % (self._filter,
198+
self._doc,
199+
self._upsert)
200+
201+
202+
class BulkWriteResult(object):
203+
"""An object wrapper for bulk API write results."""
204+
205+
__slots__ = ("__bulk_api_result", "__acknowledged")
206+
207+
def __init__(self, bulk_api_result, acknowledged):
208+
"""Create a BulkWriteResult instance.
209+
210+
:Parameters:
211+
- `bulk_api_result`: A result dict from the bulk API
212+
- `acknowledged`: Was this write result acknowledged? If ``False``
213+
then all properties of this object will raise
214+
:exc:`~pymongo.errors.InvalidOperation`.
215+
"""
216+
self.__bulk_api_result = bulk_api_result
217+
self.__acknowledged = acknowledged
218+
219+
def __raise_if_unacknowledged(self, property_name):
220+
"""Raise an exception on property access if unacknowledged."""
221+
if not self.__acknowledged:
222+
raise InvalidOperation("A value for %s is not available when "
223+
"the write is unacknowledged. Check the "
224+
"acknowledged attribute to avoid this "
225+
"error." % (property_name,))
226+
227+
@property
228+
def bulk_api_result(self):
229+
"""The raw bulk API result."""
230+
return self.__bulk_api_result
231+
232+
@property
233+
def acknowledged(self):
234+
"""Was this bulk write operation acknowledged?"""
235+
return self.__acknowledged
236+
237+
@property
238+
def inserted_count(self):
239+
"""The number of documents inserted."""
240+
self.__raise_if_unacknowledged("inserted_count")
241+
return self.__bulk_api_result.get("nInserted")
242+
243+
@property
244+
def matched_count(self):
245+
"""The number of documents matched for an update."""
246+
self.__raise_if_unacknowledged("matched_count")
247+
return self.__bulk_api_result.get("nMatched")
248+
249+
@property
250+
def modified_count(self):
251+
"""The number of documents modified.
252+
253+
.. note:: modified_count is only reported by MongoDB 2.6 and later.
254+
When connected to an earlier server version, or in certain mixed
255+
version sharding configurations, this attribute will be set to
256+
``None``.
257+
"""
258+
self.__raise_if_unacknowledged("modified_count")
259+
return self.__bulk_api_result.get("nModified")
260+
261+
@property
262+
def deleted_count(self):
263+
"""The number of documents deleted."""
264+
self.__raise_if_unacknowledged("deleted_count")
265+
return self.__bulk_api_result.get("nRemoved")
266+
267+
@property
268+
def upserted_count(self):
269+
"""The number of documents upserted."""
270+
self.__raise_if_unacknowledged("upserted_count")
271+
return self.__bulk_api_result.get("nUpserted")
272+
273+
@property
274+
def upserted_ids(self):
275+
"""A map of operation index to the _id of the upserted document."""
276+
self.__raise_if_unacknowledged("upserted_ids")
277+
if self.__bulk_api_result:
278+
return dict((upsert["index"], upsert["_id"])
279+
for upsert in self.bulk_api_result["upserted"])
280+
281+
45282
class _Run(object):
46283
"""Represents a batch of write operations.
47284
"""

pymongo/collection.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@
2626
string_type)
2727
from bson.codec_options import CodecOptions
2828
from bson.son import SON
29-
from pymongo import (bulk,
30-
common,
29+
from pymongo import (common,
3130
helpers,
3231
message)
32+
from pymongo.bulk import (BulkOperationBuilder,
33+
BulkWriteResult,
34+
_Bulk,
35+
_WriteOp)
3336
from pymongo.command_cursor import CommandCursor
3437
from pymongo.cursor import Cursor
3538
from pymongo.errors import ConfigurationError, InvalidName, OperationFailure
@@ -259,7 +262,7 @@ def initialize_unordered_bulk_op(self):
259262
260263
.. versionadded:: 2.7
261264
"""
262-
return bulk.BulkOperationBuilder(self, ordered=False)
265+
return BulkOperationBuilder(self, ordered=False)
263266

264267
def initialize_ordered_bulk_op(self):
265268
"""Initialize an ordered batch of write operations.
@@ -274,7 +277,50 @@ def initialize_ordered_bulk_op(self):
274277
275278
.. versionadded:: 2.7
276279
"""
277-
return bulk.BulkOperationBuilder(self, ordered=True)
280+
return BulkOperationBuilder(self, ordered=True)
281+
282+
def bulk_write(self, requests, ordered=True):
283+
"""Send a batch of write operations to the server.
284+
285+
This is an alternative to the fluent bulk write API provided through
286+
the :meth:`initialize_ordered_bulk_op` and
287+
:meth:`initialize_unordered_bulk_op` methods. Write operations are
288+
passed as a list using the write operation classes from the
289+
:mod:`~pymongo.bulk` module::
290+
291+
>>> # DeleteOne, UpdateOne, and UpdateMany are also available.
292+
...
293+
>>> from pymongo.bulk import InsertOne, DeleteMany, ReplaceOne
294+
>>> requests = [InsertOne({'foo': 1}), DeleteMany({'bar': 2}),
295+
... ReplaceOne({'bar': 1}, {'bim': 2}, upsert=True)]
296+
>>> coll.bulk_write(requests)
297+
298+
:Parameters:
299+
- `requests`: A list of write operations (see examples above).
300+
- `ordered` (optional): If ``True`` (the default) requests will be
301+
performed on the server serially, in the order provided. If an error
302+
occurs all remaining operations are aborted. If ``False`` requests
303+
will be performed on the server in arbitrary order, possibly in
304+
parallel, and all operations will be attempted.
305+
306+
:Returns:
307+
An instance of :class:`~pymongo.bulk.BulkWriteResult`.
308+
309+
.. versionadded:: 3.0
310+
"""
311+
if not isinstance(requests, list):
312+
raise TypeError("requests must be a list")
313+
314+
blk = _Bulk(self, ordered)
315+
for request in requests:
316+
if not isinstance(request, _WriteOp):
317+
raise TypeError("%r is not a valid request" % (request,))
318+
request._add_to_bulk(blk)
319+
320+
bulk_api_result = blk.execute(self.write_concern.document)
321+
if bulk_api_result is not None:
322+
return BulkWriteResult(bulk_api_result, True)
323+
return BulkWriteResult({}, False)
278324

279325
def save(self, to_save, manipulate=True, check_keys=True, **kwargs):
280326
"""Save a document in this collection.

0 commit comments

Comments
 (0)