diff --git a/pymongo/asynchronous/bulk.py b/pymongo/asynchronous/bulk.py index 1ea6fd60d9..91e08f61b3 100644 --- a/pymongo/asynchronous/bulk.py +++ b/pymongo/asynchronous/bulk.py @@ -87,7 +87,7 @@ def __init__( self, collection: AsyncCollection[_DocumentType], ordered: bool, - bypass_document_validation: bool, + bypass_document_validation: Optional[bool], comment: Optional[str] = None, let: Optional[Any] = None, ) -> None: @@ -516,8 +516,8 @@ async def _execute_command( if self.comment: cmd["comment"] = self.comment _csot.apply_write_concern(cmd, write_concern) - if self.bypass_doc_val: - cmd["bypassDocumentValidation"] = True + if self.bypass_doc_val is not None: + cmd["bypassDocumentValidation"] = self.bypass_doc_val if self.let is not None and run.op_type in (_DELETE, _UPDATE): cmd["let"] = self.let if session: diff --git a/pymongo/asynchronous/collection.py b/pymongo/asynchronous/collection.py index b87f207760..7fb20b7ab3 100644 --- a/pymongo/asynchronous/collection.py +++ b/pymongo/asynchronous/collection.py @@ -701,7 +701,7 @@ async def bulk_write( self, requests: Sequence[_WriteOp[_DocumentType]], ordered: bool = True, - bypass_document_validation: bool = False, + bypass_document_validation: Optional[bool] = None, session: Optional[AsyncClientSession] = None, comment: Optional[Any] = None, let: Optional[Mapping] = None, @@ -800,7 +800,7 @@ async def _insert_one( ordered: bool, write_concern: WriteConcern, op_id: Optional[int], - bypass_doc_val: bool, + bypass_doc_val: Optional[bool], session: Optional[AsyncClientSession], comment: Optional[Any] = None, ) -> Any: @@ -814,8 +814,8 @@ async def _insert_one( async def _insert_command( session: Optional[AsyncClientSession], conn: AsyncConnection, retryable_write: bool ) -> None: - if bypass_doc_val: - command["bypassDocumentValidation"] = True + if bypass_doc_val is not None: + command["bypassDocumentValidation"] = bypass_doc_val result = await conn.command( self._database.name, @@ -840,7 +840,7 @@ async def _insert_command( async def insert_one( self, document: Union[_DocumentType, RawBSONDocument], - bypass_document_validation: bool = False, + bypass_document_validation: Optional[bool] = None, session: Optional[AsyncClientSession] = None, comment: Optional[Any] = None, ) -> InsertOneResult: @@ -906,7 +906,7 @@ async def insert_many( self, documents: Iterable[Union[_DocumentType, RawBSONDocument]], ordered: bool = True, - bypass_document_validation: bool = False, + bypass_document_validation: Optional[bool] = None, session: Optional[AsyncClientSession] = None, comment: Optional[Any] = None, ) -> InsertManyResult: @@ -986,7 +986,7 @@ async def _update( write_concern: Optional[WriteConcern] = None, op_id: Optional[int] = None, ordered: bool = True, - bypass_doc_val: Optional[bool] = False, + bypass_doc_val: Optional[bool] = None, collation: Optional[_CollationIn] = None, array_filters: Optional[Sequence[Mapping[str, Any]]] = None, hint: Optional[_IndexKeyHint] = None, @@ -1041,8 +1041,8 @@ async def _update( if comment is not None: command["comment"] = comment # Update command. - if bypass_doc_val: - command["bypassDocumentValidation"] = True + if bypass_doc_val is not None: + command["bypassDocumentValidation"] = bypass_doc_val # The command result has to be published for APM unmodified # so we make a shallow copy here before adding updatedExisting. @@ -1082,7 +1082,7 @@ async def _update_retryable( write_concern: Optional[WriteConcern] = None, op_id: Optional[int] = None, ordered: bool = True, - bypass_doc_val: Optional[bool] = False, + bypass_doc_val: Optional[bool] = None, collation: Optional[_CollationIn] = None, array_filters: Optional[Sequence[Mapping[str, Any]]] = None, hint: Optional[_IndexKeyHint] = None, @@ -1128,7 +1128,7 @@ async def replace_one( filter: Mapping[str, Any], replacement: Mapping[str, Any], upsert: bool = False, - bypass_document_validation: bool = False, + bypass_document_validation: Optional[bool] = None, collation: Optional[_CollationIn] = None, hint: Optional[_IndexKeyHint] = None, session: Optional[AsyncClientSession] = None, @@ -1237,7 +1237,7 @@ async def update_one( filter: Mapping[str, Any], update: Union[Mapping[str, Any], _Pipeline], upsert: bool = False, - bypass_document_validation: bool = False, + bypass_document_validation: Optional[bool] = None, collation: Optional[_CollationIn] = None, array_filters: Optional[Sequence[Mapping[str, Any]]] = None, hint: Optional[_IndexKeyHint] = None, @@ -2948,6 +2948,7 @@ async def aggregate( returning aggregate results using a cursor. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. + - `bypassDocumentValidation` (bool): If ``True``, allows the write to opt-out of document level validation. :return: A :class:`~pymongo.asynchronous.command_cursor.AsyncCommandCursor` over the result diff --git a/pymongo/synchronous/bulk.py b/pymongo/synchronous/bulk.py index f54dcdd42d..3823ef354d 100644 --- a/pymongo/synchronous/bulk.py +++ b/pymongo/synchronous/bulk.py @@ -87,7 +87,7 @@ def __init__( self, collection: Collection[_DocumentType], ordered: bool, - bypass_document_validation: bool, + bypass_document_validation: Optional[bool], comment: Optional[str] = None, let: Optional[Any] = None, ) -> None: @@ -516,8 +516,8 @@ def _execute_command( if self.comment: cmd["comment"] = self.comment _csot.apply_write_concern(cmd, write_concern) - if self.bypass_doc_val: - cmd["bypassDocumentValidation"] = True + if self.bypass_doc_val is not None: + cmd["bypassDocumentValidation"] = self.bypass_doc_val if self.let is not None and run.op_type in (_DELETE, _UPDATE): cmd["let"] = self.let if session: diff --git a/pymongo/synchronous/collection.py b/pymongo/synchronous/collection.py index e63ed70fc2..8a71768318 100644 --- a/pymongo/synchronous/collection.py +++ b/pymongo/synchronous/collection.py @@ -700,7 +700,7 @@ def bulk_write( self, requests: Sequence[_WriteOp[_DocumentType]], ordered: bool = True, - bypass_document_validation: bool = False, + bypass_document_validation: Optional[bool] = None, session: Optional[ClientSession] = None, comment: Optional[Any] = None, let: Optional[Mapping] = None, @@ -799,7 +799,7 @@ def _insert_one( ordered: bool, write_concern: WriteConcern, op_id: Optional[int], - bypass_doc_val: bool, + bypass_doc_val: Optional[bool], session: Optional[ClientSession], comment: Optional[Any] = None, ) -> Any: @@ -813,8 +813,8 @@ def _insert_one( def _insert_command( session: Optional[ClientSession], conn: Connection, retryable_write: bool ) -> None: - if bypass_doc_val: - command["bypassDocumentValidation"] = True + if bypass_doc_val is not None: + command["bypassDocumentValidation"] = bypass_doc_val result = conn.command( self._database.name, @@ -839,7 +839,7 @@ def _insert_command( def insert_one( self, document: Union[_DocumentType, RawBSONDocument], - bypass_document_validation: bool = False, + bypass_document_validation: Optional[bool] = None, session: Optional[ClientSession] = None, comment: Optional[Any] = None, ) -> InsertOneResult: @@ -905,7 +905,7 @@ def insert_many( self, documents: Iterable[Union[_DocumentType, RawBSONDocument]], ordered: bool = True, - bypass_document_validation: bool = False, + bypass_document_validation: Optional[bool] = None, session: Optional[ClientSession] = None, comment: Optional[Any] = None, ) -> InsertManyResult: @@ -985,7 +985,7 @@ def _update( write_concern: Optional[WriteConcern] = None, op_id: Optional[int] = None, ordered: bool = True, - bypass_doc_val: Optional[bool] = False, + bypass_doc_val: Optional[bool] = None, collation: Optional[_CollationIn] = None, array_filters: Optional[Sequence[Mapping[str, Any]]] = None, hint: Optional[_IndexKeyHint] = None, @@ -1040,8 +1040,8 @@ def _update( if comment is not None: command["comment"] = comment # Update command. - if bypass_doc_val: - command["bypassDocumentValidation"] = True + if bypass_doc_val is not None: + command["bypassDocumentValidation"] = bypass_doc_val # The command result has to be published for APM unmodified # so we make a shallow copy here before adding updatedExisting. @@ -1081,7 +1081,7 @@ def _update_retryable( write_concern: Optional[WriteConcern] = None, op_id: Optional[int] = None, ordered: bool = True, - bypass_doc_val: Optional[bool] = False, + bypass_doc_val: Optional[bool] = None, collation: Optional[_CollationIn] = None, array_filters: Optional[Sequence[Mapping[str, Any]]] = None, hint: Optional[_IndexKeyHint] = None, @@ -1127,7 +1127,7 @@ def replace_one( filter: Mapping[str, Any], replacement: Mapping[str, Any], upsert: bool = False, - bypass_document_validation: bool = False, + bypass_document_validation: Optional[bool] = None, collation: Optional[_CollationIn] = None, hint: Optional[_IndexKeyHint] = None, session: Optional[ClientSession] = None, @@ -1236,7 +1236,7 @@ def update_one( filter: Mapping[str, Any], update: Union[Mapping[str, Any], _Pipeline], upsert: bool = False, - bypass_document_validation: bool = False, + bypass_document_validation: Optional[bool] = None, collation: Optional[_CollationIn] = None, array_filters: Optional[Sequence[Mapping[str, Any]]] = None, hint: Optional[_IndexKeyHint] = None, @@ -2941,6 +2941,7 @@ def aggregate( returning aggregate results using a cursor. - `collation` (optional): An instance of :class:`~pymongo.collation.Collation`. + - `bypassDocumentValidation` (bool): If ``True``, allows the write to opt-out of document level validation. :return: A :class:`~pymongo.command_cursor.CommandCursor` over the result diff --git a/test/crud/unified/bypassDocumentValidation.json b/test/crud/unified/bypassDocumentValidation.json new file mode 100644 index 0000000000..aff2d37f81 --- /dev/null +++ b/test/crud/unified/bypassDocumentValidation.json @@ -0,0 +1,493 @@ +{ + "description": "bypassDocumentValidation", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "3.2", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll" + } + } + ], + "initialData": [ + { + "collectionName": "coll", + "databaseName": "crud", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "Aggregate with $out passes bypassDocumentValidation: false", + "operations": [ + { + "object": "collection0", + "name": "aggregate", + "arguments": { + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ], + "bypassDocumentValidation": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "coll", + "pipeline": [ + { + "$sort": { + "x": 1 + } + }, + { + "$match": { + "_id": { + "$gt": 1 + } + } + }, + { + "$out": "other_test_collection" + } + ], + "bypassDocumentValidation": false + }, + "commandName": "aggregate", + "databaseName": "crud" + } + } + ] + } + ] + }, + { + "description": "BulkWrite passes bypassDocumentValidation: false", + "operations": [ + { + "object": "collection0", + "name": "bulkWrite", + "arguments": { + "requests": [ + { + "insertOne": { + "document": { + "_id": 4, + "x": 44 + } + } + } + ], + "bypassDocumentValidation": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 4, + "x": 44 + } + ], + "bypassDocumentValidation": false + } + } + } + ] + } + ] + }, + { + "description": "FindOneAndReplace passes bypassDocumentValidation: false", + "operations": [ + { + "object": "collection0", + "name": "findOneAndReplace", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "replacement": { + "x": 32 + }, + "bypassDocumentValidation": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "coll", + "query": { + "_id": { + "$gt": 1 + } + }, + "update": { + "x": 32 + }, + "bypassDocumentValidation": false + } + } + } + ] + } + ] + }, + { + "description": "FindOneAndUpdate passes bypassDocumentValidation: false", + "operations": [ + { + "object": "collection0", + "name": "findOneAndUpdate", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "bypassDocumentValidation": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "coll", + "query": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "bypassDocumentValidation": false + } + } + } + ] + } + ] + }, + { + "description": "InsertMany passes bypassDocumentValidation: false", + "operations": [ + { + "object": "collection0", + "name": "insertMany", + "arguments": { + "documents": [ + { + "_id": 4, + "x": 44 + } + ], + "bypassDocumentValidation": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 4, + "x": 44 + } + ], + "bypassDocumentValidation": false + } + } + } + ] + } + ] + }, + { + "description": "InsertOne passes bypassDocumentValidation: false", + "operations": [ + { + "object": "collection0", + "name": "insertOne", + "arguments": { + "document": { + "_id": 4, + "x": 44 + }, + "bypassDocumentValidation": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "coll", + "documents": [ + { + "_id": 4, + "x": 44 + } + ], + "bypassDocumentValidation": false + } + } + } + ] + } + ] + }, + { + "description": "ReplaceOne passes bypassDocumentValidation: false", + "operations": [ + { + "object": "collection0", + "name": "replaceOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "replacement": { + "x": 32 + }, + "bypassDocumentValidation": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "x": 32 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ], + "bypassDocumentValidation": false + } + } + } + ] + } + ] + }, + { + "description": "UpdateMany passes bypassDocumentValidation: false", + "operations": [ + { + "object": "collection0", + "name": "updateMany", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "bypassDocumentValidation": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": true, + "upsert": { + "$$unsetOrMatches": false + } + } + ], + "bypassDocumentValidation": false + } + } + } + ] + } + ] + }, + { + "description": "UpdateOne passes bypassDocumentValidation: false", + "operations": [ + { + "object": "collection0", + "name": "updateOne", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "$inc": { + "x": 1 + } + }, + "bypassDocumentValidation": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ], + "bypassDocumentValidation": false + } + } + } + ] + } + ] + } + ] +} diff --git a/test/utils_shared.py b/test/utils_shared.py index 2c52445968..e0789b6632 100644 --- a/test/utils_shared.py +++ b/test/utils_shared.py @@ -615,6 +615,10 @@ def prepare_spec_arguments(spec, arguments, opname, entity_map, with_txn_callbac # Aggregate uses "batchSize", while find uses batch_size. elif (arg_name == "batchSize" or arg_name == "allowDiskUse") and opname == "aggregate": continue + elif arg_name == "bypassDocumentValidation" and ( + opname == "aggregate" or "find_one_and" in opname + ): + continue elif arg_name == "timeoutMode": raise unittest.SkipTest("PyMongo does not support timeoutMode") # Requires boolean returnDocument.