Skip to content

Commit 8eb1aef

Browse files
committed
PYTHON-1336 Add collation support to Collection.bulk_write.
1 parent 9468c11 commit 8eb1aef

File tree

4 files changed

+132
-41
lines changed

4 files changed

+132
-41
lines changed

doc/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ Highlights:
1717
overhead of :class:`~pymongo.operations.InsertOne`,
1818
:class:`~pymongo.operations.DeleteOne`, and
1919
:class:`~pymongo.operations.DeleteMany`.
20+
- Added the `collation` option to :class:`~pymongo.operations.DeleteOne`,
21+
:class:`~pymongo.operations.DeleteMany`,
22+
:class:`~pymongo.operations.ReplaceOne`,
23+
:class:`~pymongo.operations.UpdateOne`, and
24+
:class:`~pymongo.operations.UpdateMany`.
2025

2126
Changes and Deprecations:
2227

pymongo/bulk.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -635,8 +635,8 @@ def find(self, selector, collation=None):
635635
- `selector` (dict): the selection criteria for update
636636
and remove operations.
637637
- `collation` (optional): An instance of
638-
:class:`~pymongo.collation.Collation`. This option is only supported
639-
on MongoDB 3.4 and above.
638+
:class:`~pymongo.collation.Collation`. This option is only
639+
supported on MongoDB 3.4 and above.
640640
641641
:Returns:
642642
- A :class:`BulkWriteOperation` instance, used to add

pymongo/operations.py

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,30 +54,38 @@ def __ne__(self, other):
5454
class DeleteOne(object):
5555
"""Represents a delete_one operation."""
5656

57-
__slots__ = ("_filter",)
57+
__slots__ = ("_filter", "_collation")
5858

59-
def __init__(self, filter):
59+
def __init__(self, filter, collation=None):
6060
"""Create a DeleteOne instance.
6161
6262
For use with :meth:`~pymongo.collection.Collection.bulk_write`.
6363
6464
:Parameters:
6565
- `filter`: A query that matches the document to delete.
66+
- `collation` (optional): An instance of
67+
:class:`~pymongo.collation.Collation`. This option is only
68+
supported on MongoDB 3.4 and above.
69+
70+
.. versionchanged:: 3.5
71+
Added the `collation` option.
6672
"""
6773
if filter is not None:
6874
validate_is_mapping("filter", filter)
6975
self._filter = filter
76+
self._collation = collation
7077

7178
def _add_to_bulk(self, bulkobj):
7279
"""Add this operation to the _Bulk instance `bulkobj`."""
73-
bulkobj.add_delete(self._filter, 1)
80+
bulkobj.add_delete(self._filter, 1, collation=self._collation)
7481

7582
def __repr__(self):
76-
return "DeleteOne(%r)" % (self._filter,)
83+
return "DeleteOne(%r, %r)" % (self._filter, self._collation)
7784

7885
def __eq__(self, other):
7986
if type(other) == type(self):
80-
return other._filter == self._filter
87+
return ((other._filter, other._collation) ==
88+
(self._filter, self._collation))
8189
return NotImplemented
8290

8391
def __ne__(self, other):
@@ -87,30 +95,38 @@ def __ne__(self, other):
8795
class DeleteMany(object):
8896
"""Represents a delete_many operation."""
8997

90-
__slots__ = ("_filter",)
98+
__slots__ = ("_filter", "_collation")
9199

92-
def __init__(self, filter):
100+
def __init__(self, filter, collation=None):
93101
"""Create a DeleteMany instance.
94102
95103
For use with :meth:`~pymongo.collection.Collection.bulk_write`.
96104
97105
:Parameters:
98106
- `filter`: A query that matches the documents to delete.
107+
- `collation` (optional): An instance of
108+
:class:`~pymongo.collation.Collation`. This option is only
109+
supported on MongoDB 3.4 and above.
110+
111+
.. versionchanged:: 3.5
112+
Added the `collation` option.
99113
"""
100114
if filter is not None:
101115
validate_is_mapping("filter", filter)
102116
self._filter = filter
117+
self._collation = collation
103118

104119
def _add_to_bulk(self, bulkobj):
105120
"""Add this operation to the _Bulk instance `bulkobj`."""
106-
bulkobj.add_delete(self._filter, 0)
121+
bulkobj.add_delete(self._filter, 0, collation=self._collation)
107122

108123
def __repr__(self):
109-
return "DeleteMany(%r)" % (self._filter,)
124+
return "DeleteMany(%r, %r)" % (self._filter, self._collation)
110125

111126
def __eq__(self, other):
112127
if type(other) == type(self):
113-
return other._filter == self._filter
128+
return ((other._filter, other._collation) ==
129+
(self._filter, self._collation))
114130
return NotImplemented
115131

116132
def __ne__(self, other):
@@ -120,37 +136,40 @@ def __ne__(self, other):
120136
class _UpdateOp(object):
121137
"""Private base class for update operations."""
122138

123-
__slots__ = ("_filter", "_doc", "_upsert")
139+
__slots__ = ("_filter", "_doc", "_upsert", "_collation")
124140

125-
def __init__(self, filter, doc, upsert):
141+
def __init__(self, filter, doc, upsert, collation):
126142
if filter is not None:
127143
validate_is_mapping("filter", filter)
128144
if upsert is not None:
129145
validate_boolean("upsert", upsert)
130146
self._filter = filter
131147
self._doc = doc
132148
self._upsert = upsert
149+
self._collation = collation
133150

134151
def __eq__(self, other):
135152
if type(other) == type(self):
136-
return ((other._filter, other._doc, other._upsert) ==
137-
(self._filter, self._doc, self._upsert))
153+
return (
154+
(other._filter, other._doc, other._upsert, other._collation) ==
155+
(self._filter, self._doc, self._upsert, self._collation))
138156
return NotImplemented
139157

140158
def __ne__(self, other):
141159
return not self == other
142160

143161
def __repr__(self):
144-
return "%s(%r, %r, %r)" % (
145-
self.__class__.__name__, self._filter, self._doc, self._upsert)
162+
return "%s(%r, %r, %r, %r)" % (
163+
self.__class__.__name__, self._filter, self._doc, self._upsert,
164+
self._collation)
146165

147166

148167
class ReplaceOne(_UpdateOp):
149168
"""Represents a replace_one operation."""
150169

151170
__slots__ = ()
152171

153-
def __init__(self, filter, replacement, upsert=False):
172+
def __init__(self, filter, replacement, upsert=False, collation=None):
154173
"""Create a ReplaceOne instance.
155174
156175
For use with :meth:`~pymongo.collection.Collection.bulk_write`.
@@ -160,20 +179,28 @@ def __init__(self, filter, replacement, upsert=False):
160179
- `replacement`: The new document.
161180
- `upsert` (optional): If ``True``, perform an insert if no documents
162181
match the filter.
182+
- `collation` (optional): An instance of
183+
:class:`~pymongo.collation.Collation`. This option is only
184+
supported on MongoDB 3.4 and above.
185+
186+
.. versionchanged:: 3.5
187+
Added the `collation` option.
163188
"""
164-
super(ReplaceOne, self).__init__(filter, replacement, upsert)
189+
super(ReplaceOne, self).__init__(filter, replacement, upsert,
190+
collation)
165191

166192
def _add_to_bulk(self, bulkobj):
167193
"""Add this operation to the _Bulk instance `bulkobj`."""
168-
bulkobj.add_replace(self._filter, self._doc, self._upsert)
194+
bulkobj.add_replace(self._filter, self._doc, self._upsert,
195+
collation=self._collation)
169196

170197

171198
class UpdateOne(_UpdateOp):
172199
"""Represents an update_one operation."""
173200

174201
__slots__ = ()
175202

176-
def __init__(self, filter, update, upsert=False):
203+
def __init__(self, filter, update, upsert=False, collation=None):
177204
"""Represents an update_one operation.
178205
179206
For use with :meth:`~pymongo.collection.Collection.bulk_write`.
@@ -183,20 +210,27 @@ def __init__(self, filter, update, upsert=False):
183210
- `update`: The modifications to apply.
184211
- `upsert` (optional): If ``True``, perform an insert if no documents
185212
match the filter.
213+
- `collation` (optional): An instance of
214+
:class:`~pymongo.collation.Collation`. This option is only
215+
supported on MongoDB 3.4 and above.
216+
217+
.. versionchanged:: 3.5
218+
Added the `collation` option.
186219
"""
187-
super(UpdateOne, self).__init__(filter, update, upsert)
220+
super(UpdateOne, self).__init__(filter, update, upsert, collation)
188221

189222
def _add_to_bulk(self, bulkobj):
190223
"""Add this operation to the _Bulk instance `bulkobj`."""
191-
bulkobj.add_update(self._filter, self._doc, False, self._upsert)
224+
bulkobj.add_update(self._filter, self._doc, False, self._upsert,
225+
collation=self._collation)
192226

193227

194228
class UpdateMany(_UpdateOp):
195229
"""Represents an update_many operation."""
196230

197231
__slots__ = ()
198232

199-
def __init__(self, filter, update, upsert=False):
233+
def __init__(self, filter, update, upsert=False, collation=None):
200234
"""Create an UpdateMany instance.
201235
202236
For use with :meth:`~pymongo.collection.Collection.bulk_write`.
@@ -206,12 +240,19 @@ def __init__(self, filter, update, upsert=False):
206240
- `update`: The modifications to apply.
207241
- `upsert` (optional): If ``True``, perform an insert if no documents
208242
match the filter.
243+
- `collation` (optional): An instance of
244+
:class:`~pymongo.collation.Collation`. This option is only
245+
supported on MongoDB 3.4 and above.
246+
247+
.. versionchanged:: 3.5
248+
Added the `collation` option.
209249
"""
210-
super(UpdateMany, self).__init__(filter, update, upsert)
250+
super(UpdateMany, self).__init__(filter, update, upsert, collation)
211251

212252
def _add_to_bulk(self, bulkobj):
213253
"""Add this operation to the _Bulk instance `bulkobj`."""
214-
bulkobj.add_update(self._filter, self._doc, True, self._upsert)
254+
bulkobj.add_update(self._filter, self._doc, True, self._upsert,
255+
collation=self._collation)
215256

216257

217258
class IndexModel(object):

test/test_collation.py

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
CollationMaxVariable)
2828
from pymongo.errors import ConfigurationError
2929
from pymongo.write_concern import WriteConcern
30-
from pymongo.operations import IndexModel
30+
from pymongo.operations import (DeleteMany, DeleteOne, IndexModel, ReplaceOne,
31+
UpdateMany, UpdateOne)
3132

3233

3334
class TestCollationObject(unittest.TestCase):
@@ -273,35 +274,75 @@ def test_find_and(self):
273274
collation=self.collation)
274275
self.assertCollationInLastCommand()
275276

277+
@raisesConfigurationErrorForOldMongoDB
278+
def test_bulk_write(self):
279+
self.db.test.collection.bulk_write([
280+
DeleteOne({'noCollation': 42}),
281+
DeleteMany({'noCollation': 42}),
282+
DeleteOne({'foo': 42}, collation=self.collation),
283+
DeleteMany({'foo': 42}, collation=self.collation),
284+
ReplaceOne({'noCollation': 24}, {'bar': 42}),
285+
UpdateOne({'noCollation': 84}, {'$set': {'bar': 10}}, upsert=True),
286+
UpdateMany({'noCollation': 45}, {'$set': {'bar': 42}}),
287+
ReplaceOne({'foo': 24}, {'foo': 42}, collation=self.collation),
288+
UpdateOne({'foo': 84}, {'$set': {'foo': 10}}, upsert=True,
289+
collation=self.collation),
290+
UpdateMany({'foo': 45}, {'$set': {'foo': 42}},
291+
collation=self.collation)
292+
])
293+
294+
delete_cmd = self.listener.results['started'][0].command
295+
update_cmd = self.listener.results['started'][1].command
296+
297+
def check_ops(ops):
298+
for op in ops:
299+
if 'noCollation' in op['q']:
300+
self.assertNotIn('collation', op)
301+
else:
302+
self.assertEqual(self.collation.document,
303+
op['collation'])
304+
305+
check_ops(delete_cmd['deletes'])
306+
check_ops(update_cmd['updates'])
307+
276308
@raisesConfigurationErrorForOldMongoDB
277309
def test_bulk(self):
278310
bulk = self.db.test.initialize_ordered_bulk_op()
311+
bulk.find({'noCollation': 42}).remove_one()
312+
bulk.find({'noCollation': 42}).remove()
279313
bulk.find({'foo': 42}, collation=self.collation).remove_one()
280-
# No collation for this operation.
281-
bulk.find({'bar': 24}).replace_one({'bar': 42})
282-
bulk.find({'foo': 84}, collation=self.collation).upsert()
314+
bulk.find({'foo': 42}, collation=self.collation).remove()
315+
bulk.find({'noCollation': 24}).replace_one({'bar': 42})
316+
bulk.find({'noCollation': 84}).upsert().update_one(
317+
{'$set': {'foo': 10}})
318+
bulk.find({'noCollation': 45}).update({'$set': {'bar': 42}})
319+
bulk.find({'foo': 24}, collation=self.collation).replace_one(
320+
{'foo': 42})
321+
bulk.find({'foo': 84}, collation=self.collation).upsert().update_one(
322+
{'$set': {'foo': 10}})
283323
bulk.find({'foo': 45}, collation=self.collation).update({
284324
'$set': {'foo': 42}})
285325
bulk.execute()
286326

287327
delete_cmd = self.listener.results['started'][0].command
288328
update_cmd = self.listener.results['started'][1].command
289329

290-
for op in delete_cmd['deletes']:
291-
self.assertEqual(self.collation.document,
292-
op['collation'])
330+
def check_ops(ops):
331+
for op in ops:
332+
if 'noCollation' in op['q']:
333+
self.assertNotIn('collation', op)
334+
else:
335+
self.assertEqual(self.collation.document,
336+
op['collation'])
293337

294-
for op in update_cmd['updates']:
295-
if 'foo' in op['q']:
296-
self.assertEqual(self.collation.document,
297-
op['collation'])
298-
else:
299-
self.assertNotIn('collation', op)
338+
check_ops(delete_cmd['deletes'])
339+
check_ops(update_cmd['updates'])
300340

301341
@client_context.require_version_max(3, 3, 8)
302342
def test_mixed_bulk_collation(self):
303343
bulk = self.db.test.initialize_unordered_bulk_op()
304-
bulk.find({'foo': 42}).upsert()
344+
bulk.find({'foo': 42}).upsert().update_one(
345+
{'$set': {'bar': 10}})
305346
bulk.find({'foo': 43}, collation=self.collation).remove_one()
306347
with self.assertRaises(ConfigurationError):
307348
bulk.execute()
@@ -343,6 +384,10 @@ def test_unacknowledged_write(self):
343384
{'$set': {'hello': 'moon'}})
344385
with self.assertRaises(ConfigurationError):
345386
bulk.execute()
387+
update_one = UpdateOne({'hello': 'world'}, {'$set': {'hello': 'moon'}},
388+
collation=self.collation)
389+
with self.assertRaises(ConfigurationError):
390+
collection.bulk_write([update_one])
346391

347392
@raisesConfigurationErrorForOldMongoDB
348393
def test_cursor_collation(self):

0 commit comments

Comments
 (0)