PYTHON-4933 - Allow drivers to set bypassDocumentValidation: false on… (#2227)

This commit is contained in:
Noah Stapp 2025-03-28 15:56:37 -04:00 committed by GitHub
parent e51ad27d20
commit c326161379
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 529 additions and 30 deletions

View File

@ -87,7 +87,7 @@ class _AsyncBulk:
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 @@ class _AsyncBulk:
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:

View File

@ -701,7 +701,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
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 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
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

View File

@ -87,7 +87,7 @@ class _Bulk:
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 @@ class _Bulk:
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:

View File

@ -700,7 +700,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
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 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
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 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
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 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
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 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
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 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
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 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
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 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
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 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
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 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
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 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
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

View File

@ -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
}
}
}
]
}
]
}
]
}

View File

@ -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.