From f5b44ea35fc23be9d4e72689c5a94052e97dbb0f Mon Sep 17 00:00:00 2001 From: aherlihy Date: Mon, 5 Oct 2015 14:17:13 -0400 Subject: [PATCH] PYTHON-982 - Support bypassDocumentValidation --- pymongo/bulk.py | 51 ++++++++---- pymongo/collection.py | 138 ++++++++++++++++++++++++++----- test/test_bulk.py | 36 +++++++++ test/test_collection.py | 174 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 359 insertions(+), 40 deletions(-) diff --git a/pymongo/bulk.py b/pymongo/bulk.py index 5259cb600..658eccce1 100644 --- a/pymongo/bulk.py +++ b/pymongo/bulk.py @@ -199,7 +199,7 @@ def _merge_command(run, full_result, results): class _Bulk(object): """The private guts of the bulk write API. """ - def __init__(self, collection, ordered): + def __init__(self, collection, ordered, bypass_document_validation): """Initialize a _Bulk instance. """ self.collection = collection @@ -208,6 +208,7 @@ class _Bulk(object): self.name = "%s.%s" % (collection.database.name, collection.name) self.namespace = collection.database.name + '.$cmd' self.executed = False + self.bypass_doc_val = bypass_document_validation def add_insert(self, document): """Add an insert document to the list of ops. @@ -289,6 +290,8 @@ class _Bulk(object): ('ordered', self.ordered)]) if write_concern.document: cmd['writeConcern'] = write_concern.document + if self.bypass_doc_val and sock_info.max_wire_version >= 4: + cmd['bypassDocumentValidation'] = True bwc = _BulkWriteContext(db_name, cmd, sock_info, op_id, listeners) results = _do_batched_write_command( @@ -320,26 +323,30 @@ class _Bulk(object): for run in generator: try: if run.op_type == _INSERT: - coll._insert(sock_info, - run.ops, - self.ordered, - write_concern=write_concern, - op_id=op_id) + coll._insert( + sock_info, + run.ops, + self.ordered, + write_concern=write_concern, + op_id=op_id, + bypass_doc_val=self.bypass_doc_val) elif run.op_type == _UPDATE: for operation in run.ops: doc = operation['u'] check_keys = True if doc and next(iter(doc)).startswith('$'): check_keys = False - coll._update(sock_info, - operation['q'], - doc, - operation['upsert'], - check_keys, - operation['multi'], - write_concern=write_concern, - op_id=op_id, - ordered=self.ordered) + coll._update( + sock_info, + operation['q'], + doc, + operation['upsert'], + check_keys, + operation['multi'], + write_concern=write_concern, + op_id=op_id, + ordered=self.ordered, + bypass_doc_val=self.bypass_doc_val) else: for operation in run.ops: coll._delete(sock_info, @@ -556,7 +563,8 @@ class BulkOperationBuilder(object): __slots__ = '__bulk' - def __init__(self, collection, ordered=True): + def __init__(self, collection, ordered=True, + bypass_document_validation=False): """Initialize a new BulkOperationBuilder instance. :Parameters: @@ -567,8 +575,17 @@ class BulkOperationBuilder(object): in arbitrary order (possibly in parallel on the server), reporting any errors that occurred after attempting all operations. Defaults to ``True``. + - `bypass_document_validation`: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. + + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 3.2 + Added bypass_document_validation support """ - self.__bulk = _Bulk(collection, ordered) + self.__bulk = _Bulk(collection, ordered, bypass_document_validation) def find(self, selector): """Specify selection criteria for bulk operations. diff --git a/pymongo/collection.py b/pymongo/collection.py index d03bb3d08..4df54f486 100644 --- a/pymongo/collection.py +++ b/pymongo/collection.py @@ -306,36 +306,59 @@ class Collection(common.BaseObject): write_concern or self.write_concern, read_concern or self.read_concern) - def initialize_unordered_bulk_op(self): + def initialize_unordered_bulk_op(self, bypass_document_validation=False): """Initialize an unordered batch of write operations. Operations will be performed on the server in arbitrary order, possibly in parallel. All operations will be attempted. + :Parameters: + - `bypass_document_validation`: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. + Returns a :class:`~pymongo.bulk.BulkOperationBuilder` instance. See :ref:`unordered_bulk` for examples. + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 3.2 + Added bypass_document_validation support + .. versionadded:: 2.7 """ - return BulkOperationBuilder(self, ordered=False) + return BulkOperationBuilder(self, False, bypass_document_validation) - def initialize_ordered_bulk_op(self): + def initialize_ordered_bulk_op(self, bypass_document_validation=False): """Initialize an ordered batch of write operations. Operations will be performed on the server serially, in the order provided. If an error occurs all remaining operations are aborted. + :Parameters: + - `bypass_document_validation`: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. + Returns a :class:`~pymongo.bulk.BulkOperationBuilder` instance. See :ref:`ordered_bulk` for examples. + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 3.2 + Added bypass_document_validation support + .. versionadded:: 2.7 """ - return BulkOperationBuilder(self, ordered=True) + return BulkOperationBuilder(self, True, bypass_document_validation) - def bulk_write(self, requests, ordered=True): + def bulk_write(self, requests, ordered=True, + bypass_document_validation=False): """Send a batch of write operations to the server. Requests are passed as a list of write operation instances ( @@ -379,18 +402,27 @@ class Collection(common.BaseObject): occurs all remaining operations are aborted. If ``False`` requests will be performed on the server in arbitrary order, possibly in parallel, and all operations will be attempted. + - `bypass_document_validation`: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. :Returns: An instance of :class:`~pymongo.results.BulkWriteResult`. .. seealso:: :ref:`writes-and-ids` + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 3.2 + Added bypass_document_validation support + .. versionadded:: 3.0 """ if not isinstance(requests, list): raise TypeError("requests must be a list") - blk = _Bulk(self, ordered) + blk = _Bulk(self, ordered, bypass_document_validation) for request in requests: if not isinstance(request, _WriteOp): raise TypeError("%r is not a valid request" % (request,)) @@ -448,7 +480,7 @@ class Collection(common.BaseObject): def _insert_one( self, sock_info, doc, ordered, - check_keys, manipulate, write_concern, op_id): + check_keys, manipulate, write_concern, op_id, bypass_doc_val): """Internal helper for inserting a single document.""" if manipulate: doc = self.__database._apply_incoming_manipulators(doc, self) @@ -465,6 +497,8 @@ class Collection(common.BaseObject): command['writeConcern'] = concern if sock_info.max_wire_version > 1 and acknowledged: + if bypass_doc_val and sock_info.max_wire_version >= 4: + command['bypassDocumentValidation'] = True # Insert command. result = sock_info.command(self.__database.name, command, @@ -480,12 +514,13 @@ class Collection(common.BaseObject): return doc.get('_id') def _insert(self, sock_info, docs, ordered=True, check_keys=True, - manipulate=False, write_concern=None, op_id=None): + manipulate=False, write_concern=None, op_id=None, + bypass_doc_val=False): """Internal insert helper.""" if isinstance(docs, collections.MutableMapping): return self._insert_one( sock_info, docs, ordered, - check_keys, manipulate, write_concern, op_id) + check_keys, manipulate, write_concern, op_id, bypass_doc_val) ids = [] @@ -522,6 +557,8 @@ class Collection(common.BaseObject): command['writeConcern'] = concern if op_id is None: op_id = message._randint() + if bypass_doc_val and sock_info.max_wire_version >= 4: + command['bypassDocumentValidation'] = True bwc = message._BulkWriteContext( self.database.name, command, sock_info, op_id, self.database.client._event_listeners) @@ -538,7 +575,7 @@ class Collection(common.BaseObject): self.codec_options, bwc) return ids - def insert_one(self, document): + def insert_one(self, document, bypass_document_validation=False): """Insert a single document. >>> db.test.count({'x': 1}) @@ -553,22 +590,34 @@ class Collection(common.BaseObject): - `document`: The document to insert. Must be a mutable mapping type. If the document does not have an _id field one will be added automatically. + - `bypass_document_validation`: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. :Returns: - An instance of :class:`~pymongo.results.InsertOneResult`. .. seealso:: :ref:`writes-and-ids` + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 3.2 + Added bypass_document_validation support + .. versionadded:: 3.0 """ common.validate_is_mutable_mapping("document", document) if "_id" not in document: document["_id"] = ObjectId() with self._socket_for_writes() as sock_info: - return InsertOneResult(self._insert(sock_info, document), - self.write_concern.acknowledged) + return InsertOneResult( + self._insert(sock_info, document, + bypass_doc_val=bypass_document_validation), + self.write_concern.acknowledged) - def insert_many(self, documents, ordered=True): + def insert_many(self, documents, ordered=True, + bypass_document_validation=False): """Insert an iterable of documents. >>> db.test.count() @@ -586,12 +635,21 @@ class Collection(common.BaseObject): occurs all remaining inserts are aborted. If ``False``, documents will be inserted on the server in arbitrary order, possibly in parallel, and all document inserts will be attempted. + - `bypass_document_validation`: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. :Returns: An instance of :class:`~pymongo.results.InsertManyResult`. .. seealso:: :ref:`writes-and-ids` + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 3.2 + Added bypass_document_validation support + .. versionadded:: 3.0 """ if not isinstance(documents, collections.Iterable) or not documents: @@ -606,14 +664,15 @@ class Collection(common.BaseObject): inserted_ids.append(document["_id"]) yield (message._INSERT, document) - blk = _Bulk(self, ordered) + blk = _Bulk(self, ordered, bypass_document_validation) blk.ops = [doc for doc in gen()] blk.execute(self.write_concern.document) return InsertManyResult(inserted_ids, self.write_concern.acknowledged) def _update(self, sock_info, criteria, document, upsert=False, check_keys=True, multi=False, manipulate=False, - write_concern=None, op_id=None, ordered=True): + write_concern=None, op_id=None, ordered=True, + bypass_doc_val=False): """Internal update / replace helper.""" common.validate_boolean("upsert", upsert) if manipulate: @@ -630,6 +689,8 @@ class Collection(common.BaseObject): command['writeConcern'] = concern if sock_info.max_wire_version > 1 and acknowledged: # Update command. + if bypass_doc_val and sock_info.max_wire_version >= 4: + command['bypassDocumentValidation'] = True # The command result has to be published for APM unmodified # so we make a shallow copy here before adding updatedExisting. @@ -655,7 +716,8 @@ class Collection(common.BaseObject): message.update, self.__full_name, upsert, multi, criteria, document, acknowledged, concern, check_keys, self.codec_options) - def replace_one(self, filter, replacement, upsert=False): + def replace_one(self, filter, replacement, upsert=False, + bypass_document_validation=False): """Replace a single document matching the filter. >>> for doc in db.test.find({}): @@ -690,19 +752,30 @@ class Collection(common.BaseObject): - `replacement`: The new document. - `upsert` (optional): If ``True``, perform an insert if no documents match the filter. + - `bypass_document_validation`: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 3.2 + Added bypass_document_validation support + .. versionadded:: 3.0 """ common.validate_is_mapping("filter", filter) common.validate_ok_for_replace(replacement) with self._socket_for_writes() as sock_info: - result = self._update(sock_info, filter, replacement, upsert) + result = self._update(sock_info, filter, replacement, upsert, + bypass_doc_val=bypass_document_validation) return UpdateResult(result, self.write_concern.acknowledged) - def update_one(self, filter, update, upsert=False): + def update_one(self, filter, update, upsert=False, + bypass_document_validation=False): """Update a single document matching the filter. >>> for doc in db.test.find(): @@ -728,20 +801,31 @@ class Collection(common.BaseObject): - `update`: The modifications to apply. - `upsert` (optional): If ``True``, perform an insert if no documents match the filter. + - `bypass_document_validation`: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 3.2 + Added bypass_document_validation support + .. versionadded:: 3.0 """ common.validate_is_mapping("filter", filter) common.validate_ok_for_update(update) with self._socket_for_writes() as sock_info: - result = self._update(sock_info, filter, update, - upsert, check_keys=False) + result = self._update(sock_info, filter, update, upsert, + check_keys=False, + bypass_doc_val=bypass_document_validation) return UpdateResult(result, self.write_concern.acknowledged) - def update_many(self, filter, update, upsert=False): + def update_many(self, filter, update, upsert=False, + bypass_document_validation=False): """Update one or more documents that match the filter. >>> for doc in db.test.find(): @@ -767,17 +851,27 @@ class Collection(common.BaseObject): - `update`: The modifications to apply. - `upsert` (optional): If ``True``, perform an insert if no documents match the filter. + - `bypass_document_validation`: (optional) If ``True``, allows the + write to opt-out of document level validation. Default is + ``False``. :Returns: - An instance of :class:`~pymongo.results.UpdateResult`. + .. note:: `bypass_document_validation` requires server version + **>= 3.2** + + .. versionchanged:: 3.2 + Added bypass_document_validation support + .. versionadded:: 3.0 """ common.validate_is_mapping("filter", filter) common.validate_ok_for_update(update) with self._socket_for_writes() as sock_info: result = self._update(sock_info, filter, update, upsert, - check_keys=False, multi=True) + check_keys=False, multi=True, + bypass_doc_val=bypass_document_validation) return UpdateResult(result, self.write_concern.acknowledged) def drop(self): diff --git a/test/test_bulk.py b/test/test_bulk.py index 2a31fc6df..5fca0a38a 100644 --- a/test/test_bulk.py +++ b/test/test_bulk.py @@ -138,6 +138,42 @@ class TestBulk(BulkTestBase): # No error. bulk.find({}) + @client_context.require_version_min(3, 1, 9, -1) + @client_context.require_no_auth + def test_bypass_document_validation_bulk_op(self): + + # Test insert + self.coll.insert_one({"z": 0}) + self.db.command(SON([("collMod", "test"), + ("validator", {"z": {"$gte": 0}})])) + bulk = self.coll.initialize_ordered_bulk_op( + bypass_document_validation=False) + bulk.insert({"z": -1}) # error + self.assertRaises(BulkWriteError, bulk.execute) + self.assertEqual(0, self.coll.count({"z": -1})) + + bulk = self.coll.initialize_ordered_bulk_op( + bypass_document_validation=True) + bulk.insert({"z": -1}) + bulk.execute() + self.assertEqual(1, self.coll.count({"z": -1})) + + self.coll.insert_one({"z": 0}) + self.db.command(SON([("collMod", "test"), + ("validator", {"z": {"$gte": 0}})])) + bulk = self.coll.initialize_unordered_bulk_op( + bypass_document_validation=False) + bulk.insert({"z": -1}) # error + self.assertRaises(BulkWriteError, bulk.execute) + self.assertEqual(1, self.coll.count({"z": -1})) + + bulk = self.coll.initialize_unordered_bulk_op( + bypass_document_validation=True) + bulk.insert({"z": -1}) + bulk.execute() + self.assertEqual(2, self.coll.count({"z": -1})) + self.coll.drop() + def test_insert(self): expected = { 'nMatched': 0, diff --git a/test/test_collection.py b/test/test_collection.py index f8e5581be..5e3cb9aaa 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -32,6 +32,7 @@ from bson.son import SON from pymongo import (ASCENDING, DESCENDING, GEO2D, GEOHAYSTACK, GEOSPHERE, HASHED, TEXT) from pymongo import MongoClient, monitoring +from pymongo.bulk import BulkWriteError from pymongo.collection import Collection, ReturnDocument from pymongo.command_cursor import CommandCursor from pymongo.cursor import CursorType @@ -42,7 +43,7 @@ from pymongo.errors import (DocumentTooLarge, InvalidOperation, OperationFailure) from pymongo.message import _COMMAND_OVERHEAD -from pymongo.operations import IndexModel +from pymongo.operations import * from pymongo.read_preferences import ReadPreference from pymongo.results import (InsertOneResult, InsertManyResult, @@ -726,6 +727,177 @@ class TestCollection(IntegrationTest): self.assertRaises( DocumentTooLarge, coll.delete_one, {'data': large}) + @client_context.require_version_min(3, 1, 9, -1) + @client_context.require_no_auth + def test_insert_bypass_document_validation(self): + db = self.db + db.test.drop() + db.create_collection("test", validator={"a": {"$exists": True}}) + + # Test insert_one + self.assertRaises(OperationFailure, db.test.insert_one, + {"_id": 1, "x": 100}) + result = db.test.insert_one({"_id": 1, "x": 100}, + bypass_document_validation=True) + self.assertTrue(isinstance(result, InsertOneResult)) + self.assertEqual(1, result.inserted_id) + result = db.test.insert_one({"_id":2, "a":0}) + self.assertTrue(isinstance(result, InsertOneResult)) + self.assertEqual(2, result.inserted_id) + + # Test insert_many + docs = [{"_id": i, "x": 100 - i} for i in range(3, 100)] + self.assertRaises(OperationFailure, db.test.insert_many, docs) + result = db.test.insert_many(docs, bypass_document_validation=True) + self.assertTrue(isinstance(result, InsertManyResult)) + self.assertTrue(97, len(result.inserted_ids)) + for doc in docs: + _id = doc["_id"] + self.assertTrue(isinstance(_id, int)) + self.assertTrue(_id in result.inserted_ids) + self.assertEqual(1, db.test.count({"x": doc["x"]})) + self.assertTrue(result.acknowledged) + docs = [{"_id": i, "a": 200 - i} for i in range(100, 200)] + result = db.test.insert_many(docs) + self.assertTrue(isinstance(result, InsertManyResult)) + self.assertTrue(97, len(result.inserted_ids)) + for doc in docs: + _id = doc["_id"] + self.assertTrue(isinstance(_id, int)) + self.assertTrue(_id in result.inserted_ids) + self.assertEqual(1, db.test.count({"a": doc["a"]})) + self.assertTrue(result.acknowledged) + + @client_context.require_version_min(3, 1, 9, -1) + @client_context.require_no_auth + def test_replace_bypass_document_validation(self): + db = self.db + db.test.drop() + db.create_collection("test", validator={"a": {"$exists": True}}) + + # Test replace_one + db.test.insert_one({"a": 101}) + self.assertRaises(OperationFailure, db.test.replace_one, + {"a": 101}, {"y": 1}) + self.assertEqual(0, db.test.count({"y": 1})) + self.assertEqual(1, db.test.count({"a": 101})) + db.test.replace_one({"a": 101}, {"y": 1}, + bypass_document_validation=True) + self.assertEqual(0, db.test.count({"a": 101})) + self.assertEqual(1, db.test.count({"y": 1})) + db.test.replace_one({"y": 1}, {"a": 102}) + self.assertEqual(0, db.test.count({"y": 1})) + self.assertEqual(0, db.test.count({"a": 101})) + self.assertEqual(1, db.test.count({"a": 102})) + + db.test.insert_one({"y": 1}, bypass_document_validation=True) + self.assertRaises(OperationFailure, db.test.replace_one, + {"y": 1}, {"x": 101}) + self.assertEqual(0, db.test.count({"x": 101})) + self.assertEqual(1, db.test.count({"y": 1})) + db.test.replace_one({"y": 1}, {"x": 101}, + bypass_document_validation=True) + self.assertEqual(0, db.test.count({"y": 1})) + self.assertEqual(1, db.test.count({"x": 101})) + db.test.replace_one({"x": 101}, {"a": 103}, + bypass_document_validation=False) + self.assertEqual(0, db.test.count({"x": 101})) + self.assertEqual(1, db.test.count({"a": 103})) + + @client_context.require_version_min(3, 1, 9, -1) + @client_context.require_no_auth + def test_update_bypass_document_validation(self): + db = self.db + db.test.drop() + db.test.insert_one({"z": 5}) + db.command(SON([("collMod", "test"), + ("validator", {"z": {"$gte": 0}})])) + + # Test update_one + self.assertRaises(OperationFailure, db.test.update_one, + {"z": 5}, {"$inc": {"z": -10}}) + self.assertEqual(0, db.test.count({"z": -5})) + self.assertEqual(1, db.test.count({"z": 5})) + db.test.update_one({"z": 5}, {"$inc": {"z": -10}}, + bypass_document_validation=True) + self.assertEqual(0, db.test.count({"z": 5})) + self.assertEqual(1, db.test.count({"z": -5})) + db.test.update_one({"z": -5}, {"$inc": {"z": 6}}, + bypass_document_validation=False) + self.assertEqual(1, db.test.count({"z": 1})) + self.assertEqual(0, db.test.count({"z": -5})) + + db.test.insert_one({"z": -10}, + bypass_document_validation=True) + self.assertRaises(OperationFailure, db.test.update_one, + {"z": -10}, {"$inc": {"z": 1}}) + self.assertEqual(0, db.test.count({"z": -9})) + self.assertEqual(1, db.test.count({"z": -10})) + db.test.update_one({"z": -10}, {"$inc": {"z": 1}}, + bypass_document_validation=True) + self.assertEqual(1, db.test.count({"z": -9})) + self.assertEqual(0, db.test.count({"z": -10})) + db.test.update_one({"z": -9}, {"$inc": {"z": 9}}, + bypass_document_validation=False) + self.assertEqual(0, db.test.count({"z": -9})) + self.assertEqual(1, db.test.count({"z": 0})) + + # Test update_many + db.test.insert_many([{"z": i} for i in range(3, 101)]) + db.test.insert_one({"y": 0}, + bypass_document_validation=True) + self.assertRaises(OperationFailure, db.test.update_many, {}, + {"$inc": {"z": -100}}) + self.assertEqual(100, db.test.count({"z": {"$gte": 0}})) + self.assertEqual(0, db.test.count({"z": {"$lt": 0}})) + self.assertEqual(0, db.test.count({"y": 0, "z": -100})) + db.test.update_many({"z": {"$gte": 0}}, {"$inc": {"z": -100}}, + bypass_document_validation=True) + self.assertEqual(0, db.test.count({"z": {"$gt": 0}})) + self.assertEqual(100, db.test.count({"z": {"$lte": 0}})) + db.test.update_many({"z": {"$gt": -50}}, {"$inc": {"z": 100}}, + bypass_document_validation=False) + self.assertEqual(50, db.test.count({"z": {"$gt": 0}})) + self.assertEqual(50, db.test.count({"z": {"$lt": 0}})) + + db.test.insert_many([{"z": -i} for i in range(50)], + bypass_document_validation=True) + self.assertRaises(OperationFailure, db.test.update_many, + {}, {"$inc": {"z": 1}}) + self.assertEqual(100, db.test.count({"z": {"$lte": 0}})) + self.assertEqual(50, db.test.count({"z": {"$gt": 1}})) + db.test.update_many({"z": {"$gte": 0}}, {"$inc": {"z": -100}}, + bypass_document_validation=True) + self.assertEqual(0, db.test.count({"z": {"$gt": 0}})) + self.assertEqual(150, db.test.count({"z": {"$lte": 0}})) + db.test.update_many({"z": {"$lte": 0}}, {"$inc": {"z": 100}}, + bypass_document_validation=False) + self.assertEqual(150, db.test.count({"z": {"$gte": 0}})) + self.assertEqual(0, db.test.count({"z": {"$lt": 0}})) + + @client_context.require_version_min(3, 1, 9, -1) + @client_context.require_no_auth + def test_bypass_document_validation_bulk_write(self): + db = self.db + db.test.drop() + db.create_collection("test", validator={"a": {"$gte": 0}}) + ops = [InsertOne({"a": -10}), + InsertOne({"a": -11}), + InsertOne({"a": -12}), + UpdateOne({"a": {"$lte": -10}}, {"$inc": {"a": 1}}), + UpdateMany({"a": {"$lte": -10}}, {"$inc": {"a": 1}}), + ReplaceOne({"a": {"$lte": -10}}, {"a": -1})] + db.test.bulk_write(ops, bypass_document_validation=True) + + self.assertEqual(3, db.test.count()) + self.assertEqual(1, db.test.count({"a": -11})) + self.assertEqual(1, db.test.count({"a": -1})) + self.assertEqual(1, db.test.count({"a": -9})) + + # Assert that the operations would fail without bypass_doc_val + for op in ops: + self.assertRaises(BulkWriteError, db.test.bulk_write, [op]) + def test_find_by_default_dct(self): db = self.db db.test.insert_one({'foo': 'bar'})