Merge branch 'master' of github.com:mongodb/mongo-python-driver

This commit is contained in:
Steven Silvester 2024-08-07 19:46:05 -05:00
commit af61bbd647
No known key found for this signature in database
GPG Key ID: B1BF5EC3A8B32F91
108 changed files with 14107 additions and 1594 deletions

View File

@ -2126,7 +2126,8 @@ tasks:
script: |
${PREPARE_SHELL}
export PYTHON_BINARY=/opt/mongodbtoolchain/v4/bin/python3
export LIBMONGOCRYPT_URL=https://s3.amazonaws.com/${bucket_name}/libmongocrypt/debian10/master/latest/libmongocrypt.tar.gz
export LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian11/master/latest/libmongocrypt.tar.gz
SKIP_SERVERS=1 bash ./.evergreen/setup-encryption.sh
SUCCESS=false TEST_FLE_GCP_AUTO=1 ./.evergreen/hatch.sh test:test-eg
- name: testazurekms-task
@ -3144,7 +3145,7 @@ buildvariants:
- name: testgcpkms-variant
display_name: "GCP KMS"
run_on:
- debian10-small
- debian11-small
tasks:
- name: testgcpkms_task_group
batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README

View File

@ -137,6 +137,9 @@ do
srv|SRV|initial-dns-seedlist-discovery|srv_seedlist)
cpjson initial-dns-seedlist-discovery/tests/ srv_seedlist
;;
read-write-concern|read_write_concern)
cpjson read-write-concern/tests/operation read_write_concern/operation
;;
retryable-reads|retryable_reads)
cpjson retryable-reads/tests/ retryable_reads
;;

View File

@ -1,10 +1,12 @@
#!/bin/bash
set -o errexit # Exit the script with error if any of the commands fail
HERE=$(dirname ${BASH_SOURCE:-$0})
. $DRIVERS_TOOLS/.evergreen/csfle/azurekms/setup-secrets.sh
export LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian11/master/latest/libmongocrypt.tar.gz
SKIP_SERVERS=1 bash $HERE/setup-encryption.sh
PYTHON_BINARY=/opt/mongodbtoolchain/v4/bin/python3 \
KEY_NAME="${AZUREKMS_KEYNAME}" \
KEY_VAULT_ENDPOINT="${AZUREKMS_KEYVAULTENDPOINT}" \
LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian10/master/latest/libmongocrypt.tar.gz \
SUCCESS=false TEST_FLE_AZURE_AUTO=1 \
./.evergreen/hatch.sh test:test-eg
$HERE/hatch.sh test:test-eg
bash $HERE/teardown-encryption.sh

View File

@ -1,11 +1,13 @@
#!/bin/bash
set -o errexit # Exit the script with error if any of the commands fail
HERE=$(dirname ${BASH_SOURCE:-$0})
source ${DRIVERS_TOOLS}/.evergreen/csfle/azurekms/secrets-export.sh
echo "Copying files ... begin"
export AZUREKMS_RESOURCEGROUP=${AZUREKMS_RESOURCEGROUP}
export AZUREKMS_VMNAME=${AZUREKMS_VMNAME}
export AZUREKMS_PRIVATEKEYPATH=/tmp/testazurekms_privatekey
export LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian11/master/latest/libmongocrypt.tar.gz
SKIP_SERVERS=1 bash $HERE/setup-encryption.sh
tar czf /tmp/mongo-python-driver.tgz .
# shellcheck disable=SC2088
AZUREKMS_SRC="/tmp/mongo-python-driver.tgz" AZUREKMS_DST="~/" \
@ -16,6 +18,7 @@ AZUREKMS_CMD="tar xf mongo-python-driver.tgz" \
$DRIVERS_TOOLS/.evergreen/csfle/azurekms/run-command.sh
echo "Untarring file ... end"
echo "Running test ... begin"
AZUREKMS_CMD="KEY_NAME=\"$AZUREKMS_KEYNAME\" KEY_VAULT_ENDPOINT=\"$AZUREKMS_KEYVAULTENDPOINT\" LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian10/master/latest/libmongocrypt.tar.gz SUCCESS=true TEST_FLE_AZURE_AUTO=1 ./.evergreen/hatch.sh test:test-eg" \
AZUREKMS_CMD="KEY_NAME=\"$AZUREKMS_KEYNAME\" KEY_VAULT_ENDPOINT=\"$AZUREKMS_KEYVAULTENDPOINT\" SUCCESS=true TEST_FLE_AZURE_AUTO=1 ./.evergreen/hatch.sh test:test-eg" \
$DRIVERS_TOOLS/.evergreen/csfle/azurekms/run-command.sh
echo "Running test ... end"
bash $HERE/teardown-encryption.sh

View File

@ -1,5 +1,6 @@
#!/bin/bash
set -o errexit # Exit the script with error if any of the commands fail
HERE=$(dirname ${BASH_SOURCE:-$0})
source ${DRIVERS_TOOLS}/.evergreen/csfle/gcpkms/secrets-export.sh
echo "Copying files ... begin"
@ -7,6 +8,8 @@ export GCPKMS_GCLOUD=${GCPKMS_GCLOUD}
export GCPKMS_PROJECT=${GCPKMS_PROJECT}
export GCPKMS_ZONE=${GCPKMS_ZONE}
export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME}
export LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian11/master/latest/libmongocrypt.tar.gz
SKIP_SERVERS=1 bash $HERE/setup-encryption.sh
tar czf /tmp/mongo-python-driver.tgz .
GCPKMS_SRC=/tmp/mongo-python-driver.tgz GCPKMS_DST=$GCPKMS_INSTANCENAME: $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/copy-file.sh
echo "Copying files ... end"
@ -14,5 +17,6 @@ echo "Untarring file ... begin"
GCPKMS_CMD="tar xf mongo-python-driver.tgz" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh
echo "Untarring file ... end"
echo "Running test ... begin"
GCPKMS_CMD="SUCCESS=true TEST_FLE_GCP_AUTO=1 LIBMONGOCRYPT_URL=https://s3.amazonaws.com/mciuploads/libmongocrypt/debian10/master/latest/libmongocrypt.tar.gz ./.evergreen/hatch.sh test:test-eg" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh
GCPKMS_CMD="SUCCESS=true TEST_FLE_GCP_AUTO=1 ./.evergreen/hatch.sh test:test-eg" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh
echo "Running test ... end"
bash $HERE/teardown-encryption.sh

View File

@ -121,14 +121,14 @@ if [ -n "$TEST_PYOPENSSL" ]; then
fi
if [ -n "$TEST_ENCRYPTION" ] || [ -n "$TEST_FLE_AZURE_AUTO" ] || [ -n "$TEST_FLE_GCP_AUTO" ]; then
# Check for libmongocrypt checkout.
if [ ! -d "libmongocrypt" ]; then
echo "Run encryption setup first!"
exit 1
fi
python -m pip install '.[encryption]'
# Setup encryption if necessary.
if [ ! -d "libmongocrypt" ]; then
bash ./.evergreen/setup-encryption.sh
fi
# Use the nocrypto build to avoid dependency issues with older windows/python versions.
BASE=$(pwd)/libmongocrypt/nocrypto
if [ -f "${BASE}/lib/libmongocrypt.so" ]; then

View File

@ -4,6 +4,7 @@ set -o xtrace
if [ -z "${DRIVERS_TOOLS}" ]; then
echo "Missing environment variable DRIVERS_TOOLS"
exit 1
fi
TARGET=""
@ -50,5 +51,7 @@ tar xzf libmongocrypt.tar.gz -C ./libmongocrypt
ls -la libmongocrypt
ls -la libmongocrypt/nocrypto
bash ${DRIVERS_TOOLS}/.evergreen/csfle/setup-secrets.sh
bash ${DRIVERS_TOOLS}/.evergreen/csfle/start-servers.sh
if [ -z "${SKIP_SERVERS:-}" ]; then
bash ${DRIVERS_TOOLS}/.evergreen/csfle/setup-secrets.sh
bash ${DRIVERS_TOOLS}/.evergreen/csfle/start-servers.sh
fi

417
doc/async-tutorial.rst Normal file
View File

@ -0,0 +1,417 @@
Async Tutorial
==============
.. code-block:: pycon
from pymongo import AsyncMongoClient
client = AsyncMongoClient()
await client.drop_database("test-database")
This tutorial is intended as an introduction to working with
**MongoDB** and **PyMongo** using the asynchronous API.
Prerequisites
-------------
Before we start, make sure that you have the **PyMongo** distribution
:doc:`installed <installation>`. In the Python shell, the following
should run without raising an exception:
.. code-block:: pycon
>>> import pymongo
This tutorial also assumes that a MongoDB instance is running on the
default host and port. Assuming you have `downloaded and installed
<https://www.mongodb.com/docs/manual/installation/>`_ MongoDB, you
can start it like so:
.. code-block:: bash
$ mongod
Making a Connection with AsyncMongoClient
-----------------------------------------
The first step when working with **PyMongo** is to create a
:class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` to the running **mongod**
instance. Doing so is easy:
.. code-block:: pycon
>>> from pymongo import AsyncMongoClient
>>> client = AsyncMongoClient()
The above code will connect on the default host and port. We can also
specify the host and port explicitly, as follows:
.. code-block:: pycon
>>> client = AsyncMongoClient("localhost", 27017)
Or use the MongoDB URI format:
.. code-block:: pycon
>>> client = AsyncMongoClient("mongodb://localhost:27017/")
By default, :class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` only connects to the database on its first operation.
To explicitly connect before performing an operation, use :meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.aconnect`:
.. code-block:: pycon
>>> client = await AsyncMongoClient().aconnect()
Getting a Database
------------------
A single instance of MongoDB can support multiple independent
`databases <https://www.mongodb.com/docs/manual/core/databases-and-collections>`_. When
working with PyMongo you access databases using attribute style access
on :class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` instances:
.. code-block:: pycon
>>> db = client.test_database
If your database name is such that using attribute style access won't
work (like ``test-database``), you can use dictionary style access
instead:
.. code-block:: pycon
>>> db = client["test-database"]
Getting a Collection
--------------------
A `collection <https://www.mongodb.com/docs/manual/core/databases-and-collections>`_ is a
group of documents stored in MongoDB, and can be thought of as roughly
the equivalent of a table in a relational database. Getting a
collection in PyMongo works the same as getting a database:
.. code-block:: pycon
>>> collection = db.test_collection
or (using dictionary style access):
.. code-block:: pycon
>>> collection = db["test-collection"]
An important note about collections (and databases) in MongoDB is that
they are created lazily - none of the above commands have actually
performed any operations on the MongoDB server. Collections and
databases are created when the first document is inserted into them.
Documents
---------
Data in MongoDB is represented (and stored) using JSON-style
documents. In PyMongo we use dictionaries to represent documents. As
an example, the following dictionary might be used to represent a blog
post:
.. code-block:: pycon
>>> import datetime
>>> post = {
... "author": "Mike",
... "text": "My first blog post!",
... "tags": ["mongodb", "python", "pymongo"],
... "date": datetime.datetime.now(tz=datetime.timezone.utc),
... }
Note that documents can contain native Python types (like
:class:`datetime.datetime` instances) which will be automatically
converted to and from the appropriate `BSON
<https://bsonspec.org/>`_ types.
Inserting a Document
--------------------
To insert a document into a collection we can use the
:meth:`~pymongo.asynchronous.collection.AsyncCollection.insert_one` method:
.. code-block:: pycon
>>> posts = db.posts
>>> post_id = (await posts.insert_one(post)).inserted_id
>>> post_id
ObjectId('...')
When a document is inserted a special key, ``"_id"``, is automatically
added if the document doesn't already contain an ``"_id"`` key. The value
of ``"_id"`` must be unique across the
collection. :meth:`~pymongo.asynchronous.collection.AsyncCollection.insert_one` returns an
instance of :class:`~pymongo.results.InsertOneResult`. For more information
on ``"_id"``, see the `documentation on _id
<https://www.mongodb.com/docs/manual/reference/method/ObjectId/>`_.
After inserting the first document, the *posts* collection has
actually been created on the server. We can verify this by listing all
of the collections in our database:
.. code-block:: pycon
>>> await db.list_collection_names()
['posts']
Getting a Single Document With :meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one`
------------------------------------------------------------------------------------------------
The most basic type of query that can be performed in MongoDB is
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one`. This method returns a
single document matching a query (or ``None`` if there are no
matches). It is useful when you know there is only one matching
document, or are only interested in the first match. Here we use
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one` to get the first
document from the posts collection:
.. code-block:: pycon
>>> import pprint
>>> pprint.pprint(await posts.find_one())
{'_id': ObjectId('...'),
'author': 'Mike',
'date': datetime.datetime(...),
'tags': ['mongodb', 'python', 'pymongo'],
'text': 'My first blog post!'}
The result is a dictionary matching the one that we inserted previously.
.. note:: The returned document contains an ``"_id"``, which was
automatically added on insert.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one` also supports querying
on specific elements that the resulting document must match. To limit
our results to a document with author "Mike" we do:
.. code-block:: pycon
>>> pprint.pprint(await posts.find_one({"author": "Mike"}))
{'_id': ObjectId('...'),
'author': 'Mike',
'date': datetime.datetime(...),
'tags': ['mongodb', 'python', 'pymongo'],
'text': 'My first blog post!'}
If we try with a different author, like "Eliot", we'll get no result:
.. code-block:: pycon
>>> await posts.find_one({"author": "Eliot"})
>>>
.. _async-querying-by-objectid:
Querying By ObjectId
--------------------
We can also find a post by its ``_id``, which in our example is an ObjectId:
.. code-block:: pycon
>>> post_id
ObjectId(...)
>>> pprint.pprint(await posts.find_one({"_id": post_id}))
{'_id': ObjectId('...'),
'author': 'Mike',
'date': datetime.datetime(...),
'tags': ['mongodb', 'python', 'pymongo'],
'text': 'My first blog post!'}
Note that an ObjectId is not the same as its string representation:
.. code-block:: pycon
>>> post_id_as_str = str(post_id)
>>> await posts.find_one({"_id": post_id_as_str}) # No result
>>>
A common task in web applications is to get an ObjectId from the
request URL and find the matching document. It's necessary in this
case to **convert the ObjectId from a string** before passing it to
``find_one``::
from bson.objectid import ObjectId
# The web framework gets post_id from the URL and passes it as a string
async def get(post_id):
# Convert from string to ObjectId:
document = await client.db.collection.find_one({'_id': ObjectId(post_id)})
.. seealso:: :ref:`web-application-querying-by-objectid`
Bulk Inserts
------------
In order to make querying a little more interesting, let's insert a
few more documents. In addition to inserting a single document, we can
also perform *bulk insert* operations, by passing a list as the
first argument to :meth:`~pymongo.asynchronous.collection.AsyncCollection.insert_many`.
This will insert each document in the list, sending only a single
command to the server:
.. code-block:: pycon
>>> new_posts = [
... {
... "author": "Mike",
... "text": "Another post!",
... "tags": ["bulk", "insert"],
... "date": datetime.datetime(2009, 11, 12, 11, 14),
... },
... {
... "author": "Eliot",
... "title": "MongoDB is fun",
... "text": "and pretty easy too!",
... "date": datetime.datetime(2009, 11, 10, 10, 45),
... },
... ]
>>> result = await posts.insert_many(new_posts)
>>> result.inserted_ids
[ObjectId('...'), ObjectId('...')]
There are a couple of interesting things to note about this example:
- The result from :meth:`~pymongo.asynchronous.collection.AsyncCollection.insert_many` now
returns two :class:`~bson.objectid.ObjectId` instances, one for
each inserted document.
- ``new_posts[1]`` has a different "shape" than the other posts -
there is no ``"tags"`` field and we've added a new field,
``"title"``. This is what we mean when we say that MongoDB is
*schema-free*.
Querying for More Than One Document
-----------------------------------
To get more than a single document as the result of a query we use the
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find`
method. :meth:`~pymongo.asynchronous.collection.AsyncCollection.find` returns a
:class:`~pymongo.asynchronous.cursor.AsyncCursor` instance, which allows us to iterate
over all matching documents. For example, we can iterate over every
document in the ``posts`` collection:
.. code-block:: pycon
>>> async for post in posts.find():
... pprint.pprint(post)
...
{'_id': ObjectId('...'),
'author': 'Mike',
'date': datetime.datetime(...),
'tags': ['mongodb', 'python', 'pymongo'],
'text': 'My first blog post!'}
{'_id': ObjectId('...'),
'author': 'Mike',
'date': datetime.datetime(...),
'tags': ['bulk', 'insert'],
'text': 'Another post!'}
{'_id': ObjectId('...'),
'author': 'Eliot',
'date': datetime.datetime(...),
'text': 'and pretty easy too!',
'title': 'MongoDB is fun'}
Just like we did with :meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one`,
we can pass a document to :meth:`~pymongo.asynchronous.collection.AsyncCollection.find`
to limit the returned results. Here, we get only those documents whose
author is "Mike":
.. code-block:: pycon
>>> async for post in posts.find({"author": "Mike"}):
... pprint.pprint(post)
...
{'_id': ObjectId('...'),
'author': 'Mike',
'date': datetime.datetime(...),
'tags': ['mongodb', 'python', 'pymongo'],
'text': 'My first blog post!'}
{'_id': ObjectId('...'),
'author': 'Mike',
'date': datetime.datetime(...),
'tags': ['bulk', 'insert'],
'text': 'Another post!'}
Counting
--------
If we just want to know how many documents match a query we can
perform a :meth:`~pymongo.asynchronous.collection.AsyncCollection.count_documents` operation
instead of a full query. We can get a count of all of the documents
in a collection:
.. code-block:: pycon
>>> await posts.count_documents({})
3
or just of those documents that match a specific query:
.. code-block:: pycon
>>> await posts.count_documents({"author": "Mike"})
2
Range Queries
-------------
MongoDB supports many different types of `advanced queries
<https://www.mongodb.com/docs/manual/reference/operator/>`_. As an
example, lets perform a query where we limit results to posts older
than a certain date, but also sort the results by author:
.. code-block:: pycon
>>> d = datetime.datetime(2009, 11, 12, 12)
>>> async for post in posts.find({"date": {"$lt": d}}).sort("author"):
... pprint.pprint(post)
...
{'_id': ObjectId('...'),
'author': 'Eliot',
'date': datetime.datetime(...),
'text': 'and pretty easy too!',
'title': 'MongoDB is fun'}
{'_id': ObjectId('...'),
'author': 'Mike',
'date': datetime.datetime(...),
'tags': ['bulk', 'insert'],
'text': 'Another post!'}
Here we use the special ``"$lt"`` operator to do a range query, and
also call :meth:`~pymongo.asynchronous.cursor.AsyncCursor.sort` to sort the results
by author.
Indexing
--------
Adding indexes can help accelerate certain queries and can also add additional
functionality to querying and storing documents. In this example, we'll
demonstrate how to create a `unique index
<http://mongodb.com/docs/manual/core/index-unique/>`_ on a key that rejects
documents whose value for that key already exists in the index.
First, we'll need to create the index:
.. code-block:: pycon
>>> result = await db.profiles.create_index([("user_id", pymongo.ASCENDING)], unique=True)
>>> sorted(list(await db.profiles.index_information()))
['_id_', 'user_id_1']
Notice that we have two indexes now: one is the index on ``_id`` that MongoDB
creates automatically, and the other is the index on ``user_id`` we just
created.
Now let's set up some user profiles:
.. code-block:: pycon
>>> user_profiles = [{"user_id": 211, "name": "Luke"}, {"user_id": 212, "name": "Ziltoid"}]
>>> result = await db.profiles.insert_many(user_profiles)
The index prevents us from inserting a document whose ``user_id`` is already in
the collection:
.. code-block:: pycon
>>> new_profile = {"user_id": 213, "name": "Drew"}
>>> duplicate_profile = {"user_id": 212, "name": "Tommy"}
>>> result = await db.profiles.insert_one(new_profile) # This is fine.
>>> result = await db.profiles.insert_one(duplicate_profile)
Traceback (most recent call last):
DuplicateKeyError: E11000 duplicate key error index: test_database.profiles.$user_id_1 dup key: { : 212 }
.. seealso:: The MongoDB documentation on `indexes <https://www.mongodb.com/docs/manual/indexes/>`_

View File

@ -101,3 +101,4 @@ The following is a list of people who have contributed to
- Casey Clements (caseyclements)
- Ivan Lukyanchikov (ilukyanchikov)
- Terry Patterson
- Romain Morotti

View File

@ -16,6 +16,9 @@ everything you need to know to use **PyMongo**.
:doc:`tutorial`
Start here for a quick overview.
:doc:`async-tutorial`
Start here for a quick overview of the asynchronous API.
:doc:`examples/index`
Examples of how to perform specific tasks.
@ -121,6 +124,7 @@ Indices and tables
atlas
installation
tutorial
async-tutorial
examples/index
faq
compatibility-policy

View File

@ -0,0 +1,77 @@
# Copyright 2024-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you
# may not use this file except in compliance with the License. You
# may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the License for the specific language governing
# permissions and limitations under the License.
"""Constants, types, and classes shared across Client Bulk Write API implementations."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, NoReturn
from pymongo.errors import ClientBulkWriteException, OperationFailure
from pymongo.helpers_shared import _get_wce_doc
if TYPE_CHECKING:
from pymongo.typings import _DocumentOut
def _merge_command(
ops: list[tuple[str, Mapping[str, Any]]],
offset: int,
full_result: MutableMapping[str, Any],
result: Mapping[str, Any],
) -> None:
"""Merge result of a single bulk write batch into the full result."""
if result.get("error"):
full_result["error"] = result["error"]
full_result["nInserted"] += result.get("nInserted", 0)
full_result["nDeleted"] += result.get("nDeleted", 0)
full_result["nMatched"] += result.get("nMatched", 0)
full_result["nModified"] += result.get("nModified", 0)
full_result["nUpserted"] += result.get("nUpserted", 0)
write_errors = result.get("writeErrors")
if write_errors:
for doc in write_errors:
# Leave the server response intact for APM.
replacement = doc.copy()
original_index = doc["idx"] + offset
replacement["idx"] = original_index
# Add the failed operation to the error document.
replacement["op"] = ops[original_index][1]
full_result["writeErrors"].append(replacement)
wce = _get_wce_doc(result)
if wce:
full_result["writeConcernErrors"].append(wce)
def _throw_client_bulk_write_exception(
full_result: _DocumentOut, verbose_results: bool
) -> NoReturn:
"""Raise a ClientBulkWriteException from the full result."""
# retryWrites on MMAPv1 should raise an actionable error.
if full_result["writeErrors"]:
full_result["writeErrors"].sort(key=lambda error: error["idx"])
err = full_result["writeErrors"][0]
code = err["code"]
msg = err["errmsg"]
if code == 20 and msg.startswith("Transaction numbers"):
errmsg = (
"This MongoDB deployment does not support "
"retryable writes. Please add retryWrites=false "
"to your connection string."
)
raise OperationFailure(errmsg, code, full_result)
raise ClientBulkWriteException(full_result, verbose_results)

View File

@ -149,10 +149,7 @@ class MovingMinimum:
def add_sample(self, sample: float) -> None:
if sample < 0:
# Likely system time change while waiting for hello response
# and not using time.monotonic. Ignore it, the next one will
# probably be valid.
return
raise ValueError(f"duration cannot be negative {sample}")
self.samples.append(sample)
def get(self) -> float:

View File

@ -40,8 +40,8 @@ class _AggregationCommand:
"""The internal abstract base class for aggregation cursors.
Should not be called directly by application developers. Use
:meth:`pymongo.collection.AsyncCollection.aggregate`, or
:meth:`pymongo.database.AsyncDatabase.aggregate` instead.
:meth:`pymongo.asynchronous.collection.AsyncCollection.aggregate`, or
:meth:`pymongo.asynchronous.database.AsyncDatabase.aggregate` instead.
"""
def __init__(

View File

@ -91,9 +91,9 @@ class AsyncChangeStream(Generic[_DocumentType]):
"""The internal abstract base class for change stream cursors.
Should not be called directly by application developers. Use
:meth:`pymongo.collection.AsyncCollection.watch`,
:meth:`pymongo.database.AsyncDatabase.watch`, or
:meth:`pymongo.mongo_client.AsyncMongoClient.watch` instead.
:meth:`pymongo.asynchronous.collection.AsyncCollection.watch`,
:meth:`pymongo.asynchronous.database.AsyncDatabase.watch`, or
:meth:`pymongo.asynchronous.mongo_client.AsyncMongoClient.watch` instead.
.. versionadded:: 3.6
.. seealso:: The MongoDB documentation on `changeStreams <https://mongodb.com/docs/manual/changeStreams/>`_.
@ -166,7 +166,7 @@ class AsyncChangeStream(Generic[_DocumentType]):
@property
def _client(self) -> AsyncMongoClient:
"""The client against which the aggregation commands for
this ChangeStream will be run.
this AsyncChangeStream will be run.
"""
raise NotImplementedError
@ -204,7 +204,7 @@ class AsyncChangeStream(Generic[_DocumentType]):
return options
def _aggregation_pipeline(self) -> list[dict[str, Any]]:
"""Return the full aggregation pipeline for this ChangeStream."""
"""Return the full aggregation pipeline for this AsyncChangeStream."""
options = self._change_stream_options()
full_pipeline: list = [{"$changeStream": options}]
full_pipeline.extend(self._pipeline)
@ -238,7 +238,7 @@ class AsyncChangeStream(Generic[_DocumentType]):
async def _run_aggregation_cmd(
self, session: Optional[AsyncClientSession], explicit_session: bool
) -> AsyncCommandCursor:
"""Run the full aggregation pipeline for this ChangeStream and return
"""Run the full aggregation pipeline for this AsyncChangeStream and return
the corresponding AsyncCommandCursor.
"""
cmd = self._aggregation_command_class(
@ -272,7 +272,7 @@ class AsyncChangeStream(Generic[_DocumentType]):
self._cursor = await self._create_cursor()
async def close(self) -> None:
"""Close this ChangeStream."""
"""Close this AsyncChangeStream."""
self._closed = True
await self._cursor.close()
@ -299,27 +299,27 @@ class AsyncChangeStream(Generic[_DocumentType]):
try:
resume_token = None
pipeline = [{'$match': {'operationType': 'insert'}}]
async with db.collection.watch(pipeline) as stream:
async with await db.collection.watch(pipeline) as stream:
async for insert_change in stream:
print(insert_change)
resume_token = stream.resume_token
except pymongo.errors.PyMongoError:
# The ChangeStream encountered an unrecoverable error or the
# The AsyncChangeStream encountered an unrecoverable error or the
# resume attempt failed to recreate the cursor.
if resume_token is None:
# There is no usable resume token because there was a
# failure during ChangeStream initialization.
# failure during AsyncChangeStream initialization.
logging.error('...')
else:
# Use the interrupted ChangeStream's resume token to create
# a new ChangeStream. The new stream will continue from the
# Use the interrupted AsyncChangeStream's resume token to create
# a new AsyncChangeStream. The new stream will continue from the
# last seen insert change without missing any events.
async with db.collection.watch(
async with await db.collection.watch(
pipeline, resume_after=resume_token) as stream:
async for insert_change in stream:
print(insert_change)
Raises :exc:`StopIteration` if this ChangeStream is closed.
Raises :exc:`StopIteration` if this AsyncChangeStream is closed.
"""
while self.alive:
doc = await self.try_next()
@ -348,10 +348,10 @@ class AsyncChangeStream(Generic[_DocumentType]):
This method returns the next change document without waiting
indefinitely for the next change. For example::
async with db.collection.watch() as stream:
async with await db.collection.watch() as stream:
while stream.alive:
change = await stream.try_next()
# Note that the ChangeStream's resume token may be updated
# Note that the AsyncChangeStream's resume token may be updated
# even when no changes are returned.
print("Current resume token: %r" % (stream.resume_token,))
if change is not None:
@ -447,7 +447,7 @@ class AsyncCollectionChangeStream(AsyncChangeStream[_DocumentType]):
"""A change stream that watches changes on a single collection.
Should not be called directly by application developers. Use
helper method :meth:`pymongo.collection.AsyncCollection.watch` instead.
helper method :meth:`pymongo.asynchronous.collection.AsyncCollection.watch` instead.
.. versionadded:: 3.7
"""
@ -467,7 +467,7 @@ class AsyncDatabaseChangeStream(AsyncChangeStream[_DocumentType]):
"""A change stream that watches changes on all collections in a database.
Should not be called directly by application developers. Use
helper method :meth:`pymongo.database.AsyncDatabase.watch` instead.
helper method :meth:`pymongo.asynchronous.database.AsyncDatabase.watch` instead.
.. versionadded:: 3.7
"""
@ -487,7 +487,7 @@ class AsyncClusterChangeStream(AsyncDatabaseChangeStream[_DocumentType]):
"""A change stream that watches changes on all collections in the cluster.
Should not be called directly by application developers. Use
helper method :meth:`pymongo.mongo_client.AsyncMongoClient.watch` instead.
helper method :meth:`pymongo.asynchronous.mongo_client.AsyncMongoClient.watch` instead.
.. versionadded:: 3.7
"""

View File

@ -0,0 +1,788 @@
# Copyright 2024-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The client-level bulk write operations interface.
.. versionadded:: 4.9
"""
from __future__ import annotations
import copy
import datetime
import logging
from collections.abc import MutableMapping
from itertools import islice
from typing import (
TYPE_CHECKING,
Any,
Mapping,
Optional,
Type,
Union,
)
from bson.objectid import ObjectId
from bson.raw_bson import RawBSONDocument
from pymongo import _csot, common
from pymongo.asynchronous.client_session import AsyncClientSession, _validate_session_write_concern
from pymongo.asynchronous.collection import AsyncCollection
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.database import AsyncDatabase
from pymongo.asynchronous.helpers import _handle_reauth
if TYPE_CHECKING:
from pymongo.asynchronous.mongo_client import AsyncMongoClient
from pymongo.asynchronous.pool import AsyncConnection
from pymongo._client_bulk_shared import (
_merge_command,
_throw_client_bulk_write_exception,
)
from pymongo.common import (
validate_is_document_type,
validate_ok_for_replace,
validate_ok_for_update,
)
from pymongo.errors import (
ConfigurationError,
ConnectionFailure,
InvalidOperation,
NotPrimaryError,
OperationFailure,
WaitQueueTimeoutError,
)
from pymongo.helpers_shared import _RETRYABLE_ERROR_CODES
from pymongo.logger import _COMMAND_LOGGER, _CommandStatusMessage, _debug_log
from pymongo.message import (
_ClientBulkWriteContext,
_convert_client_bulk_exception,
_convert_exception,
_convert_write_result,
_randint,
)
from pymongo.read_preferences import ReadPreference
from pymongo.results import (
ClientBulkWriteResult,
DeleteResult,
InsertOneResult,
UpdateResult,
)
from pymongo.typings import _DocumentOut, _Pipeline
from pymongo.write_concern import WriteConcern
_IS_SYNC = False
class _AsyncClientBulk:
"""The private guts of the client-level bulk write API."""
def __init__(
self,
client: AsyncMongoClient,
write_concern: WriteConcern,
ordered: bool = True,
bypass_document_validation: Optional[bool] = None,
comment: Optional[str] = None,
let: Optional[Any] = None,
verbose_results: bool = False,
) -> None:
"""Initialize a _AsyncClientBulk instance."""
self.client = client
self.write_concern = write_concern
self.let = let
if self.let is not None:
common.validate_is_document_type("let", self.let)
self.ordered = ordered
self.bypass_doc_val = bypass_document_validation
self.comment = comment
self.verbose_results = verbose_results
self.ops: list[tuple[str, Mapping[str, Any]]] = []
self.idx_offset: int = 0
self.total_ops: int = 0
self.executed = False
self.uses_upsert = False
self.uses_collation = False
self.uses_array_filters = False
self.uses_hint_update = False
self.uses_hint_delete = False
self.is_retryable = self.client.options.retry_writes
self.retrying = False
self.started_retryable_write = False
@property
def bulk_ctx_class(self) -> Type[_ClientBulkWriteContext]:
return _ClientBulkWriteContext
def add_insert(self, namespace: str, document: _DocumentOut) -> None:
"""Add an insert document to the list of ops."""
validate_is_document_type("document", document)
# Generate ObjectId client side.
if not (isinstance(document, RawBSONDocument) or "_id" in document):
document["_id"] = ObjectId()
cmd = {"insert": namespace, "document": document}
self.ops.append(("insert", cmd))
self.total_ops += 1
def add_update(
self,
namespace: str,
selector: Mapping[str, Any],
update: Union[Mapping[str, Any], _Pipeline],
multi: bool = False,
upsert: Optional[bool] = None,
collation: Optional[Mapping[str, Any]] = None,
array_filters: Optional[list[Mapping[str, Any]]] = None,
hint: Union[str, dict[str, Any], None] = None,
) -> None:
"""Create an update document and add it to the list of ops."""
validate_ok_for_update(update)
cmd = {
"update": namespace,
"filter": selector,
"updateMods": update,
"multi": multi,
}
if upsert is not None:
self.uses_upsert = True
cmd["upsert"] = upsert
if array_filters is not None:
self.uses_array_filters = True
cmd["arrayFilters"] = array_filters
if hint is not None:
self.uses_hint_update = True
cmd["hint"] = hint
if collation is not None:
self.uses_collation = True
cmd["collation"] = collation
if multi:
# A bulk_write containing an update_many is not retryable.
self.is_retryable = False
self.ops.append(("update", cmd))
self.total_ops += 1
def add_replace(
self,
namespace: str,
selector: Mapping[str, Any],
replacement: Mapping[str, Any],
upsert: Optional[bool] = None,
collation: Optional[Mapping[str, Any]] = None,
hint: Union[str, dict[str, Any], None] = None,
) -> None:
"""Create a replace document and add it to the list of ops."""
validate_ok_for_replace(replacement)
cmd = {
"update": namespace,
"filter": selector,
"updateMods": replacement,
"multi": False,
}
if upsert is not None:
self.uses_upsert = True
cmd["upsert"] = upsert
if hint is not None:
self.uses_hint_update = True
cmd["hint"] = hint
if collation is not None:
self.uses_collation = True
cmd["collation"] = collation
self.ops.append(("replace", cmd))
self.total_ops += 1
def add_delete(
self,
namespace: str,
selector: Mapping[str, Any],
multi: bool,
collation: Optional[Mapping[str, Any]] = None,
hint: Union[str, dict[str, Any], None] = None,
) -> None:
"""Create a delete document and add it to the list of ops."""
cmd = {"delete": namespace, "filter": selector, "multi": multi}
if hint is not None:
self.uses_hint_delete = True
cmd["hint"] = hint
if collation is not None:
self.uses_collation = True
cmd["collation"] = collation
if multi:
# A bulk_write containing an update_many is not retryable.
self.is_retryable = False
self.ops.append(("delete", cmd))
self.total_ops += 1
@_handle_reauth
async def write_command(
self,
bwc: _ClientBulkWriteContext,
cmd: MutableMapping[str, Any],
request_id: int,
msg: Union[bytes, dict[str, Any]],
op_docs: list[Mapping[str, Any]],
ns_docs: list[Mapping[str, Any]],
client: AsyncMongoClient,
) -> dict[str, Any]:
"""A proxy for AsyncConnection.write_command that handles event publishing."""
cmd["ops"] = op_docs
cmd["nsInfo"] = ns_docs
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.STARTED,
command=cmd,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
)
if bwc.publish:
bwc._start(cmd, request_id, op_docs, ns_docs)
try:
reply = await bwc.conn.write_command(request_id, msg, bwc.codec) # type: ignore[misc, arg-type]
duration = datetime.datetime.now() - bwc.start_time
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.SUCCEEDED,
durationMS=duration,
reply=reply,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
)
if bwc.publish:
bwc._succeed(request_id, reply, duration) # type: ignore[arg-type]
except Exception as exc:
duration = datetime.datetime.now() - bwc.start_time
if isinstance(exc, (NotPrimaryError, OperationFailure)):
failure: _DocumentOut = exc.details # type: ignore[assignment]
else:
failure = _convert_exception(exc)
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.FAILED,
durationMS=duration,
failure=failure,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
isServerSideError=isinstance(exc, OperationFailure),
)
if bwc.publish:
bwc._fail(request_id, failure, duration)
# Top-level error will be embedded in ClientBulkWriteException.
reply = {"error": exc}
finally:
bwc.start_time = datetime.datetime.now()
return reply # type: ignore[return-value]
async def unack_write(
self,
bwc: _ClientBulkWriteContext,
cmd: MutableMapping[str, Any],
request_id: int,
msg: bytes,
op_docs: list[Mapping[str, Any]],
ns_docs: list[Mapping[str, Any]],
client: AsyncMongoClient,
) -> Optional[Mapping[str, Any]]:
"""A proxy for AsyncConnection.unack_write that handles event publishing."""
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.STARTED,
command=cmd,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
)
if bwc.publish:
cmd = bwc._start(cmd, request_id, op_docs, ns_docs)
try:
result = await bwc.conn.unack_write(msg, bwc.max_bson_size) # type: ignore[func-returns-value, misc, override]
duration = datetime.datetime.now() - bwc.start_time
if result is not None:
reply = _convert_write_result(bwc.name, cmd, result) # type: ignore[arg-type]
else:
# Comply with APM spec.
reply = {"ok": 1}
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.SUCCEEDED,
durationMS=duration,
reply=reply,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
)
if bwc.publish:
bwc._succeed(request_id, reply, duration)
except Exception as exc:
duration = datetime.datetime.now() - bwc.start_time
if isinstance(exc, OperationFailure):
failure: _DocumentOut = _convert_write_result(bwc.name, cmd, exc.details) # type: ignore[arg-type]
elif isinstance(exc, NotPrimaryError):
failure = exc.details # type: ignore[assignment]
else:
failure = _convert_exception(exc)
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.FAILED,
durationMS=duration,
failure=failure,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
isServerSideError=isinstance(exc, OperationFailure),
)
if bwc.publish:
assert bwc.start_time is not None
bwc._fail(request_id, failure, duration)
# Top-level error will be embedded in ClientBulkWriteException.
reply = {"error": exc}
finally:
bwc.start_time = datetime.datetime.now()
return result # type: ignore[return-value]
async def _execute_batch_unack(
self,
bwc: _ClientBulkWriteContext,
cmd: dict[str, Any],
ops: list[tuple[str, Mapping[str, Any]]],
) -> tuple[list[Mapping[str, Any]], list[Mapping[str, Any]]]:
"""Executes a batch of bulkWrite server commands (unack)."""
request_id, msg, to_send_ops, to_send_ns = bwc.batch_command(cmd, ops)
await self.unack_write(bwc, cmd, request_id, msg, to_send_ops, to_send_ns, self.client) # type: ignore[arg-type]
return to_send_ops, to_send_ns
async def _execute_batch(
self,
bwc: _ClientBulkWriteContext,
cmd: dict[str, Any],
ops: list[tuple[str, Mapping[str, Any]]],
) -> tuple[dict[str, Any], list[Mapping[str, Any]], list[Mapping[str, Any]]]:
"""Executes a batch of bulkWrite server commands (ack)."""
request_id, msg, to_send_ops, to_send_ns = bwc.batch_command(cmd, ops)
result = await self.write_command(
bwc, cmd, request_id, msg, to_send_ops, to_send_ns, self.client
) # type: ignore[arg-type]
await self.client._process_response(result, bwc.session) # type: ignore[arg-type]
return result, to_send_ops, to_send_ns # type: ignore[return-value]
async def _process_results_cursor(
self,
full_result: MutableMapping[str, Any],
result: MutableMapping[str, Any],
conn: AsyncConnection,
session: Optional[AsyncClientSession],
) -> None:
"""Internal helper for processing the server reply command cursor."""
if result.get("cursor"):
coll = AsyncCollection(
database=AsyncDatabase(self.client, "admin"),
name="$cmd.bulkWrite",
)
cmd_cursor = AsyncCommandCursor(
coll,
result["cursor"],
conn.address,
session=session,
explicit_session=session is not None,
comment=self.comment,
)
await cmd_cursor._maybe_pin_connection(conn)
# Iterate the cursor to get individual write results.
try:
async for doc in cmd_cursor:
original_index = doc["idx"] + self.idx_offset
op_type, op = self.ops[original_index]
if not doc["ok"]:
result["writeErrors"].append(doc)
if self.ordered:
return
# Record individual write result.
if doc["ok"] and self.verbose_results:
if op_type == "insert":
inserted_id = op["document"]["_id"]
res = InsertOneResult(inserted_id, acknowledged=True) # type: ignore[assignment]
if op_type in ["update", "replace"]:
op_type = "update"
res = UpdateResult(doc, acknowledged=True, in_client_bulk=True) # type: ignore[assignment]
if op_type == "delete":
res = DeleteResult(doc, acknowledged=True) # type: ignore[assignment]
full_result[f"{op_type}Results"][original_index] = res
except Exception as exc:
# Attempt to close the cursor, then raise top-level error.
if cmd_cursor.alive:
await cmd_cursor.close()
result["error"] = _convert_client_bulk_exception(exc)
async def _execute_command(
self,
write_concern: WriteConcern,
session: Optional[AsyncClientSession],
conn: AsyncConnection,
op_id: int,
retryable: bool,
full_result: MutableMapping[str, Any],
final_write_concern: Optional[WriteConcern] = None,
) -> None:
"""Internal helper for executing batches of bulkWrite commands."""
db_name = "admin"
cmd_name = "bulkWrite"
listeners = self.client._event_listeners
# AsyncConnection.command validates the session, but we use
# AsyncConnection.write_command
conn.validate_session(self.client, session)
bwc = self.bulk_ctx_class(
db_name,
cmd_name,
conn,
op_id,
listeners, # type: ignore[arg-type]
session,
self.client.codec_options,
)
while self.idx_offset < self.total_ops:
# If this is the last possible batch, use the
# final write concern.
if self.total_ops - self.idx_offset <= bwc.max_write_batch_size:
write_concern = final_write_concern or write_concern
# Construct the server command, specifying the relevant options.
cmd = {"bulkWrite": 1}
cmd["errorsOnly"] = not self.verbose_results
cmd["ordered"] = self.ordered # type: ignore[assignment]
not_in_transaction = session and not session.in_transaction
if not_in_transaction or not session:
_csot.apply_write_concern(cmd, write_concern)
if self.bypass_doc_val is not None:
cmd["bypassDocumentValidation"] = self.bypass_doc_val
if self.comment:
cmd["comment"] = self.comment # type: ignore[assignment]
if self.let:
cmd["let"] = self.let
if session:
# Start a new retryable write unless one was already
# started for this command.
if retryable and not self.started_retryable_write:
session._start_retryable_write()
self.started_retryable_write = True
session._apply_to(cmd, retryable, ReadPreference.PRIMARY, conn)
conn.send_cluster_time(cmd, session, self.client)
conn.add_server_api(cmd)
# CSOT: apply timeout before encoding the command.
conn.apply_timeout(self.client, cmd)
ops = islice(self.ops, self.idx_offset, None)
# Run as many ops as possible in one server command.
if write_concern.acknowledged:
raw_result, to_send_ops, _ = await self._execute_batch(bwc, cmd, ops) # type: ignore[arg-type]
result = copy.deepcopy(raw_result)
# Top-level server/network error.
if result.get("error"):
error = result["error"]
retryable_top_level_error = (
isinstance(error.details, dict)
and error.details.get("code", 0) in _RETRYABLE_ERROR_CODES
)
retryable_network_error = isinstance(
error, ConnectionFailure
) and not isinstance(error, (NotPrimaryError, WaitQueueTimeoutError))
# Synthesize the full bulk result without modifying the
# current one because this write operation may be retried.
if retryable and (retryable_top_level_error or retryable_network_error):
full = copy.deepcopy(full_result)
_merge_command(self.ops, self.idx_offset, full, result)
_throw_client_bulk_write_exception(full, self.verbose_results)
else:
_merge_command(self.ops, self.idx_offset, full_result, result)
_throw_client_bulk_write_exception(full_result, self.verbose_results)
result["error"] = None
result["writeErrors"] = []
if result.get("nErrors", 0) < len(to_send_ops):
full_result["anySuccessful"] = True
# Top-level command error.
if not result["ok"]:
result["error"] = raw_result
_merge_command(self.ops, self.idx_offset, full_result, result)
break
if retryable:
# Retryable writeConcernErrors halt the execution of this batch.
wce = result.get("writeConcernError", {})
if wce.get("code", 0) in _RETRYABLE_ERROR_CODES:
# Synthesize the full bulk result without modifying the
# current one because this write operation may be retried.
full = copy.deepcopy(full_result)
_merge_command(self.ops, self.idx_offset, full, result)
_throw_client_bulk_write_exception(full, self.verbose_results)
# Process the server reply as a command cursor.
await self._process_results_cursor(full_result, result, conn, session)
# Merge this batch's results with the full results.
_merge_command(self.ops, self.idx_offset, full_result, result)
# We're no longer in a retry once a command succeeds.
self.retrying = False
self.started_retryable_write = False
else:
to_send_ops, _ = await self._execute_batch_unack(bwc, cmd, ops) # type: ignore[arg-type]
self.idx_offset += len(to_send_ops)
# We halt execution if we hit a top-level error,
# or an individual error in an ordered bulk write.
if full_result["error"] or (self.ordered and full_result["writeErrors"]):
break
async def execute_command(
self,
session: Optional[AsyncClientSession],
operation: str,
) -> MutableMapping[str, Any]:
"""Execute commands with w=1 WriteConcern."""
full_result: MutableMapping[str, Any] = {
"anySuccessful": False,
"error": None,
"writeErrors": [],
"writeConcernErrors": [],
"nInserted": 0,
"nUpserted": 0,
"nMatched": 0,
"nModified": 0,
"nDeleted": 0,
"insertResults": {},
"updateResults": {},
"deleteResults": {},
}
op_id = _randint()
async def retryable_bulk(
session: Optional[AsyncClientSession],
conn: AsyncConnection,
retryable: bool,
) -> None:
if conn.max_wire_version < 25:
raise InvalidOperation(
"MongoClient.bulk_write requires MongoDB server version 8.0+."
)
await self._execute_command(
self.write_concern,
session,
conn,
op_id,
retryable,
full_result,
)
await self.client._retryable_write(
self.is_retryable,
retryable_bulk,
session,
operation,
bulk=self,
operation_id=op_id,
)
if full_result["error"] or full_result["writeErrors"] or full_result["writeConcernErrors"]:
_throw_client_bulk_write_exception(full_result, self.verbose_results)
return full_result
async def execute_command_unack_unordered(
self,
conn: AsyncConnection,
) -> None:
"""Execute commands with OP_MSG and w=0 writeConcern, unordered."""
db_name = "admin"
cmd_name = "bulkWrite"
listeners = self.client._event_listeners
op_id = _randint()
bwc = self.bulk_ctx_class(
db_name,
cmd_name,
conn,
op_id,
listeners, # type: ignore[arg-type]
None,
self.client.codec_options,
)
while self.idx_offset < self.total_ops:
# Construct the server command, specifying the relevant options.
cmd = {"bulkWrite": 1}
cmd["errorsOnly"] = not self.verbose_results
cmd["ordered"] = self.ordered # type: ignore[assignment]
if self.bypass_doc_val is not None:
cmd["bypassDocumentValidation"] = self.bypass_doc_val
cmd["writeConcern"] = {"w": 0} # type: ignore[assignment]
if self.comment:
cmd["comment"] = self.comment # type: ignore[assignment]
if self.let:
cmd["let"] = self.let
conn.add_server_api(cmd)
ops = islice(self.ops, self.idx_offset, None)
# Run as many ops as possible in one server command.
to_send_ops, _ = await self._execute_batch_unack(bwc, cmd, ops) # type: ignore[arg-type]
self.idx_offset += len(to_send_ops)
async def execute_command_unack_ordered(
self,
conn: AsyncConnection,
) -> None:
"""Execute commands with OP_MSG and w=0 WriteConcern, ordered."""
full_result: MutableMapping[str, Any] = {
"anySuccessful": False,
"error": None,
"writeErrors": [],
"writeConcernErrors": [],
"nInserted": 0,
"nUpserted": 0,
"nMatched": 0,
"nModified": 0,
"nDeleted": 0,
"insertResults": {},
"updateResults": {},
"deleteResults": {},
}
# Ordered bulk writes have to be acknowledged so that we stop
# processing at the first error, even when the application
# specified unacknowledged writeConcern.
initial_write_concern = WriteConcern()
op_id = _randint()
try:
await self._execute_command(
initial_write_concern,
None,
conn,
op_id,
False,
full_result,
self.write_concern,
)
except OperationFailure:
pass
async def execute_no_results(
self,
conn: AsyncConnection,
) -> None:
"""Execute all operations, returning no results (w=0)."""
if self.uses_collation:
raise ConfigurationError("Collation is unsupported for unacknowledged writes.")
if self.uses_array_filters:
raise ConfigurationError("arrayFilters is unsupported for unacknowledged writes.")
# Cannot have both unacknowledged writes and bypass document validation.
if self.bypass_doc_val is not None:
raise OperationFailure(
"Cannot set bypass_document_validation with unacknowledged write concern"
)
if self.ordered:
return await self.execute_command_unack_ordered(conn)
return await self.execute_command_unack_unordered(conn)
async def execute(
self,
session: Optional[AsyncClientSession],
operation: str,
) -> Any:
"""Execute operations."""
if not self.ops:
raise InvalidOperation("No operations to execute")
if self.executed:
raise InvalidOperation("Bulk operations can only be executed once.")
self.executed = True
session = _validate_session_write_concern(session, self.write_concern)
if not self.write_concern.acknowledged:
async with await self.client._conn_for_writes(session, operation) as connection:
if connection.max_wire_version < 25:
raise InvalidOperation(
"MongoClient.bulk_write requires MongoDB server version 8.0+."
)
await self.execute_no_results(connection)
return ClientBulkWriteResult(None, False, False) # type: ignore[arg-type]
result = await self.execute_command(session, operation)
return ClientBulkWriteResult(
result,
self.write_concern.acknowledged,
self.verbose_results,
)

View File

@ -102,7 +102,7 @@ Snapshot Reads
MongoDB 5.0 adds support for snapshot reads. Snapshot reads are requested by
passing the ``snapshot`` option to
:meth:`~pymongo.mongo_client.AsyncMongoClient.start_session`.
:meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.start_session`.
If ``snapshot`` is True, all read operations that use this session read data
from the same snapshot timestamp. The server chooses the latest
majority-committed snapshot timestamp when executing the first read operation
@ -123,11 +123,11 @@ Snapshot Reads Limitations
Snapshot reads sessions are incompatible with ``causal_consistency=True``.
Only the following read operations are supported in a snapshot reads session:
- :meth:`~pymongo.collection.AsyncCollection.find`
- :meth:`~pymongo.collection.AsyncCollection.find_one`
- :meth:`~pymongo.collection.AsyncCollection.aggregate`
- :meth:`~pymongo.collection.AsyncCollection.count_documents`
- :meth:`~pymongo.collection.AsyncCollection.distinct` (on unsharded collections)
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.find`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.aggregate`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.count_documents`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.distinct` (on unsharded collections)
Classes
=======
@ -492,7 +492,7 @@ class AsyncClientSession:
Should not be initialized directly by application developers - to create a
:class:`AsyncClientSession`, call
:meth:`~pymongo.mongo_client.AsyncMongoClient.start_session`.
:meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.start_session`.
"""
def __init__(
@ -550,7 +550,7 @@ class AsyncClientSession:
@property
def client(self) -> AsyncMongoClient:
"""The :class:`~pymongo.mongo_client.AsyncMongoClient` this session was
"""The :class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` this session was
created from.
"""
return self._client
@ -898,7 +898,7 @@ class AsyncClientSession:
"""Update the cluster time for this session.
:param cluster_time: The
:data:`~pymongo.client_session.AsyncClientSession.cluster_time` from
:data:`~pymongo.asynchronous.client_session.AsyncClientSession.cluster_time` from
another `AsyncClientSession` instance.
"""
if not isinstance(cluster_time, _Mapping):
@ -919,7 +919,7 @@ class AsyncClientSession:
"""Update the operation time for this session.
:param operation_time: The
:data:`~pymongo.client_session.AsyncClientSession.operation_time` from
:data:`~pymongo.asynchronous.client_session.AsyncClientSession.operation_time` from
another `AsyncClientSession` instance.
"""
if not isinstance(operation_time, Timestamp):
@ -1133,7 +1133,7 @@ class _ServerSessionPool(collections.deque):
def get_server_session(self, session_timeout_minutes: Optional[int]) -> _ServerSession:
# Although the Driver Sessions Spec says we only clear stale sessions
# in return_server_session, PyMongo can't take a lock when returning
# sessions from a __del__ method (like in Cursor.__die), so it can't
# sessions from a __del__ method (like in AsyncCursor.__die), so it can't
# clear stale sessions there. In case many sessions were returned via
# __del__, check for stale sessions here too.
self._clear_stale(session_timeout_minutes)

View File

@ -111,8 +111,8 @@ _WriteOp = Union[
class ReturnDocument:
"""An enum used with
:meth:`~pymongo.collection.AsyncCollection.find_one_and_replace` and
:meth:`~pymongo.collection.AsyncCollection.find_one_and_update`.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one_and_replace` and
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one_and_update`.
"""
BEFORE = False
@ -155,7 +155,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:class:`str`. Raises :class:`~pymongo.errors.InvalidName` if `name` is
not a valid collection name. Any additional keyword arguments will be used
as options passed to the create command. See
:meth:`~pymongo.database.AsyncDatabase.create_collection` for valid
:meth:`~pymongo.asynchronous.database.AsyncDatabase.create_collection` for valid
options.
If `create` is ``True``, `collation` is specified, or any additional
@ -207,8 +207,8 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
.. versionchanged:: 3.0
Added the codec_options, read_preference, and write_concern options.
Removed the uuid_subtype attribute.
:class:`~pymongo.collection.Collection` no longer returns an
instance of :class:`~pymongo.collection.Collection` for attribute
:class:`~pymongo.asynchronous.collection.AsyncCollection` no longer returns an
instance of :class:`~pymongo.asynchronous.collection.AsyncCollection` for attribute
names with leading underscores. You must use dict-style lookups
instead::
@ -249,7 +249,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
if create or kwargs:
if _IS_SYNC:
warnings.warn(
"The `create` and `kwargs` arguments to Collection are deprecated and will be removed in PyMongo 5.0",
"The `create` and `kwargs` arguments to AsyncCollection are deprecated and will be removed in PyMongo 5.0",
DeprecationWarning,
stacklevel=2,
)
@ -321,7 +321,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
@property
def database(self) -> AsyncDatabase[_DocumentType]:
"""The :class:`~pymongo.database.AsyncDatabase` that this
"""The :class:`~pymongo.asynchronous.database.AsyncDatabase` that this
:class:`AsyncCollection` is a part of.
"""
return self._database
@ -346,19 +346,19 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param codec_options: An instance of
:class:`~bson.codec_options.CodecOptions`. If ``None`` (the
default) the :attr:`codec_options` of this :class:`Collection`
default) the :attr:`codec_options` of this :class:`AsyncCollection`
is used.
:param read_preference: The read preference to use. If
``None`` (the default) the :attr:`read_preference` of this
:class:`Collection` is used. See :mod:`~pymongo.read_preferences`
:class:`AsyncCollection` is used. See :mod:`~pymongo.read_preferences`
for options.
:param write_concern: An instance of
:class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the
default) the :attr:`write_concern` of this :class:`Collection`
default) the :attr:`write_concern` of this :class:`AsyncCollection`
is used.
:param read_concern: An instance of
:class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the
default) the :attr:`read_concern` of this :class:`Collection`
default) the :attr:`read_concern` of this :class:`AsyncCollection`
is used.
"""
return AsyncCollection(
@ -384,7 +384,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
__iter__ = None
def __next__(self) -> NoReturn:
raise TypeError(f"'{type(self).__name__}' object is not iterable")
raise TypeError("'AsyncCollection' object is not iterable")
next = __next__
@ -393,7 +393,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
if "." not in self._name:
raise TypeError(
f"'{type(self).__name__}' object is not callable. If you "
"meant to call the '%s' method on a 'Database' "
"meant to call the '%s' method on an 'AsyncDatabase' "
"object it is failing because no such method "
"exists." % self._name
)
@ -427,7 +427,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
.. code-block:: python
async with db.collection.watch() as stream:
async with await db.collection.watch() as stream:
async for change in stream:
print(change)
@ -443,11 +443,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
.. code-block:: python
try:
async with db.collection.watch([{"$match": {"operationType": "insert"}}]) as stream:
async with await db.coll.watch([{"$match": {"operationType": "insert"}}]) as stream:
async for insert_change in stream:
print(insert_change)
except pymongo.errors.PyMongoError:
# The ChangeStream encountered an unrecoverable error or the
# The AsyncChangeStream encountered an unrecoverable error or the
# resume attempt failed to recreate the cursor.
logging.error("...")
@ -455,7 +455,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
`change streams specification`_.
.. note:: Using this helper method is preferred to directly calling
:meth:`~pymongo.collection.AsyncCollection.aggregate` with a
:meth:`~pymongo.asynchronous.collection.AsyncCollection.aggregate` with a
``$changeStream`` stage, for the purpose of supporting
resumability.
@ -493,7 +493,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
the specified :class:`~bson.timestamp.Timestamp`. Requires
MongoDB >= 4.0.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param start_after: The same as `resume_after` except that
`start_after` can resume notifications after an invalidate event.
This option and `resume_after` are mutually exclusive.
@ -580,7 +580,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param collation` (optional) - An instance of
:class:`~pymongo.collation.Collation`.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param retryable_write: True if this command is a retryable
write.
:param user_fields: Response fields that should be decoded
@ -689,7 +689,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:class:`~pymongo.operations.DeleteOne`, or
:class:`~pymongo.operations.DeleteMany`).
>>> for doc in db.test.find({}):
>>> async for doc in db.test.find({}):
... print(doc)
...
{'x': 1, '_id': ObjectId('54f62e60fba5226811f634ef')}
@ -699,7 +699,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
>>> from pymongo import InsertOne, DeleteOne, ReplaceOne
>>> requests = [InsertOne({'y': 1}), DeleteOne({'x': 1}),
... ReplaceOne({'w': 1}, {'z': 1}, upsert=True)]
>>> result = db.test.bulk_write(requests)
>>> result = await db.test.bulk_write(requests)
>>> result.inserted_count
1
>>> result.deleted_count
@ -708,7 +708,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
0
>>> result.upserted_ids
{2: ObjectId('54f62ee28891e756a6e1abd5')}
>>> for doc in db.test.find({}):
>>> async for doc in db.test.find({}):
... print(doc)
...
{'x': 1, '_id': ObjectId('54f62e60fba5226811f634f0')}
@ -725,7 +725,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
write to opt-out of document level validation. Default is
``False``.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param let: Map of parameter names and values. Values must be
@ -834,7 +834,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
write to opt-out of document level validation. Default is
``False``.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
@ -903,7 +903,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
write to opt-out of document level validation. Default is
``False``.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
@ -1138,11 +1138,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param hint: An index to use to support the query
predicate specified either by its string name, or in the same
format as passed to
:meth:`~pymongo.collection.AsyncCollection.create_index` (e.g.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.2 and above.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param let: Map of parameter names and values. Values must be
constant or closed expressions that do not reference document
fields. Parameters can then be accessed as variables in an
@ -1247,11 +1247,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param hint: An index to use to support the query
predicate specified either by its string name, or in the same
format as passed to
:meth:`~pymongo.collection.AsyncCollection.create_index` (e.g.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.2 and above.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param let: Map of parameter names and values. Values must be
constant or closed expressions that do not reference document
fields. Parameters can then be accessed as variables in an
@ -1347,11 +1347,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param hint: An index to use to support the query
predicate specified either by its string name, or in the same
format as passed to
:meth:`~pymongo.collection.AsyncCollection.create_index` (e.g.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.2 and above.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param let: Map of parameter names and values. Values must be
constant or closed expressions that do not reference document
fields. Parameters can then be accessed as variables in an
@ -1407,10 +1407,10 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
comment: Optional[Any] = None,
encrypted_fields: Optional[Mapping[str, Any]] = None,
) -> None:
"""Alias for :meth:`~pymongo.database.AsyncDatabase.drop_collection`.
"""Alias for :meth:`~pymongo.asynchronous.database.AsyncDatabase.drop_collection`.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param encrypted_fields: **(BETA)** Document that describes the encrypted fields for
@ -1565,11 +1565,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param hint: An index to use to support the query
predicate specified either by its string name, or in the same
format as passed to
:meth:`~pymongo.collection.AsyncCollection.create_index` (e.g.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.4 and above.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param let: Map of parameter names and values. Values must be
constant or closed expressions that do not reference document
fields. Parameters can then be accessed as variables in an
@ -1630,11 +1630,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param hint: An index to use to support the query
predicate specified either by its string name, or in the same
format as passed to
:meth:`~pymongo.collection.AsyncCollection.create_index` (e.g.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.4 and above.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param let: Map of parameter names and values. Values must be
constant or closed expressions that do not reference document
fields. Parameters can then be accessed as variables in an
@ -1722,7 +1722,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
Raises :class:`TypeError` if any of the arguments are of
improper type. Returns an instance of
:class:`~pymongo.cursor.AsyncCursor` corresponding to this query.
:class:`~pymongo.asynchronous.cursor.AsyncCursor` corresponding to this query.
The :meth:`find` method obeys the :attr:`read_preference` of
this :class:`AsyncCollection`.
@ -1736,7 +1736,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
always be returned. Use a dict to exclude fields from
the result (e.g. projection={'_id': False}).
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param skip: the number of documents to omit (from
the start of the result set) when returning the results
:param limit: the maximum number of results to
@ -1772,7 +1772,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param sort: a list of (key, direction) pairs
specifying the sort order for this query. See
:meth:`~pymongo.cursor.Cursor.sort` for details.
:meth:`~pymongo.asynchronous.cursor.AsyncCursor.sort` for details.
:param allow_partial_results: if True, mongos will return
partial results if some shards are down instead of returning an
error.
@ -1790,32 +1790,32 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
cursor from returning a document more than once because of an
intervening write operation.
:param hint: An index, in the same format as passed to
:meth:`~pymongo.collection.AsyncCollection.create_index` (e.g.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` (e.g.
``[('field', ASCENDING)]``). Pass this as an alternative to calling
:meth:`~pymongo.cursor.Cursor.hint` on the cursor to tell Mongo the
:meth:`~pymongo.asynchronous.cursor.AsyncCursor.hint` on the cursor to tell Mongo the
proper index to use for the query.
:param max_time_ms: Specifies a time limit for a query
operation. If the specified time is exceeded, the operation will be
aborted and :exc:`~pymongo.errors.ExecutionTimeout` is raised. Pass
this as an alternative to calling
:meth:`~pymongo.cursor.AsyncCursor.max_time_ms` on the cursor.
:meth:`~pymongo.asynchronous.cursor.AsyncCursor.max_time_ms` on the cursor.
:param max_scan: **DEPRECATED** - The maximum number of
documents to scan. Pass this as an alternative to calling
:meth:`~pymongo.cursor.AsyncCursor.max_scan` on the cursor.
:meth:`~pymongo.asynchronous.cursor.AsyncCursor.max_scan` on the cursor.
:param min: A list of field, limit pairs specifying the
inclusive lower bound for all keys of a specific index in order.
Pass this as an alternative to calling
:meth:`~pymongo.cursor.AsyncCursor.min` on the cursor. ``hint`` must
:meth:`~pymongo.asynchronous.cursor.AsyncCursor.min` on the cursor. ``hint`` must
also be passed to ensure the query utilizes the correct index.
:param max: A list of field, limit pairs specifying the
exclusive upper bound for all keys of a specific index in order.
Pass this as an alternative to calling
:meth:`~pymongo.cursor.Cursor.max` on the cursor. ``hint`` must
:meth:`~pymongo.asynchronous.cursor.AsyncCursor.max` on the cursor. ``hint`` must
also be passed to ensure the query utilizes the correct index.
:param comment: A string to attach to the query to help
interpret and trace the operation in the server logs and in profile
data. Pass this as an alternative to calling
:meth:`~pymongo.cursor.AsyncCursor.comment` on the cursor.
:meth:`~pymongo.asynchronous.cursor.AsyncCursor.comment` on the cursor.
:param allow_disk_use: if True, MongoDB may use temporary
disk files to store data exceeding the system memory limit while
processing a blocking sort operation. The option has no effect if
@ -1834,7 +1834,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
- A :class:`~pymongo.cursor.AsyncCursor` instance created with the
:attr:`~pymongo.cursor.CursorType.EXHAUST` cursor_type requires an
exclusive :class:`~socket.socket` connection to MongoDB. If the
:class:`~pymongo.cursor.AsyncCursor` is discarded without being
:class:`~pymongo.asynchronous.cursor.AsyncCursor` is discarded without being
completely iterated the underlying :class:`~socket.socket`
connection will be closed and discarded without being returned to
the connection pool.
@ -1899,7 +1899,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
"""Query the database and retrieve batches of raw BSON.
Similar to the :meth:`find` method but returns a
:class:`~pymongo.cursor.AsyncRawBatchCursor`.
:class:`~pymongo.asynchronous.cursor.AsyncRawBatchCursor`.
This example demonstrates how to work with raw batches, but in practice
raw batches should be passed to an external library that can decode
@ -2069,7 +2069,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
to count in the collection. Can be an empty document to count all
documents.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: See list of options above.
@ -2144,7 +2144,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param indexes: A list of :class:`~pymongo.operations.IndexModel`
instances.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: optional arguments to the createIndexes
@ -2153,7 +2153,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
.. note:: The :attr:`~pymongo.collection.AsyncCollection.write_concern` of
.. note:: The :attr:`~pymongo.asynchronous.collection.AsyncCollection.write_concern` of
this collection is automatically applied to this operation.
.. versionchanged:: 3.6
@ -2181,7 +2181,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param indexes: A list of :class:`~pymongo.operations.IndexModel`
instances.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param kwargs: optional arguments to the createIndexes
command (like maxTimeMS) can be passed as keyword arguments.
"""
@ -2290,13 +2290,13 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
option is silently ignored by the server and unique index builds
using the option will fail if a duplicate value is detected.
.. note:: The :attr:`~pymongo.collection.Collection.write_concern` of
.. note:: The :attr:`~pymongo.asynchronous.collection.AsyncCollection.write_concern` of
this collection is automatically applied to this operation.
:param keys: a single key or a list of (key, direction)
pairs specifying the index to create
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: any additional index creation
@ -2347,13 +2347,13 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
Raises OperationFailure on an error.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: optional arguments to the createIndexes
command (like maxTimeMS) can be passed as keyword arguments.
.. note:: The :attr:`~pymongo.collection.AsyncCollection.write_concern` of
.. note:: The :attr:`~pymongo.asynchronous.collection.AsyncCollection.write_concern` of
this collection is automatically applied to this operation.
.. versionchanged:: 3.6
@ -2394,7 +2394,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param index_or_name: index (or name of index) to drop
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: optional arguments to the createIndexes
@ -2402,7 +2402,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
.. note:: The :attr:`~pymongo.collection.AsyncCollection.write_concern` of
.. note:: The :attr:`~pymongo.asynchronous.collection.AsyncCollection.write_concern` of
this collection is automatically applied to this operation.
@ -2453,17 +2453,17 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
) -> AsyncCommandCursor[MutableMapping[str, Any]]:
"""Get a cursor over the index documents for this collection.
>>> async for index in db.test.list_indexes():
>>> async for index in await db.test.list_indexes():
... print(index)
...
SON([('v', 2), ('key', SON([('_id', 1)])), ('name', '_id_')])
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:return: An instance of :class:`~pymongo.command_cursor.AsyncCommandCursor`.
:return: An instance of :class:`~pymongo.asynchronous.command_cursor.AsyncCommandCursor`.
.. versionchanged:: 4.1
Added ``comment`` parameter.
@ -2541,14 +2541,14 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
``"name"`` keys, which are cleaned. Example output might look
like this:
>>> db.test.create_index("x", unique=True)
>>> await db.test.create_index("x", unique=True)
'x_1'
>>> db.test.index_information()
>>> await db.test.index_information()
{'_id_': {'key': [('_id', 1)]},
'x_1': {'unique': True, 'key': [('x', 1)]}}
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
@ -2579,11 +2579,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
for. Only indexes with matching index names will be returned.
If not given, all search indexes for the current collection
will be returned.
:param session: a :class:`~pymongo.client_session.AsyncClientSession`.
:param session: a :class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:return: A :class:`~pymongo.command_cursor.AsyncCommandCursor` over the result
:return: A :class:`~pymongo.asynchronous.command_cursor.AsyncCommandCursor` over the result
set.
.. note:: requires a MongoDB server version 7.0+ Atlas cluster.
@ -2633,7 +2633,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
instance or a dictionary with a model "definition" and optional
"name".
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: optional arguments to the createSearchIndexes
@ -2659,7 +2659,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
"""Create multiple search indexes for the current collection.
:param models: A list of :class:`~pymongo.operations.SearchIndexModel` instances.
:param session: a :class:`~pymongo.client_session.AsyncClientSession`.
:param session: a :class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: optional arguments to the createSearchIndexes
@ -2716,7 +2716,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param name: The name of the search index to be deleted.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: optional arguments to the dropSearchIndexes
@ -2752,7 +2752,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param name: The name of the search index to be updated.
:param definition: The new search index definition.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: optional arguments to the updateSearchIndexes
@ -2783,12 +2783,12 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
"""Get the options set on this collection.
Returns a dictionary of options and their values - see
:meth:`~pymongo.database.AsyncDatabase.create_collection` for more
:meth:`~pymongo.asynchronous.database.AsyncDatabase.create_collection` for more
information on the possible options. Returns an empty
dictionary if the collection has not been created yet.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
@ -2874,12 +2874,12 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
instead. An example is included in the :ref:`aggregate-examples`
documentation.
.. note:: The :attr:`~pymongo.collection.AsyncCollection.write_concern` of
.. note:: The :attr:`~pymongo.asynchronous.collection.AsyncCollection.write_concern` of
this collection is automatically applied to this operation.
:param pipeline: a list of aggregation pipeline stages
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param let: A dict of parameter names and values. Values must be
constant or closed expressions that do not reference document
fields. Parameters can then be accessed as variables in an
@ -2905,7 +2905,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:class:`~pymongo.collation.Collation`.
:return: A :class:`~pymongo.command_cursor.AsyncCommandCursor` over the result
:return: A :class:`~pymongo.asynchronous.command_cursor.AsyncCommandCursor` over the result
set.
.. versionchanged:: 4.1
@ -2928,7 +2928,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
Apply this collection's write concern automatically to this operation
when connected to MongoDB >= 3.4. Support the `collation` option.
.. versionchanged:: 3.0
The :meth:`aggregate` method always returns a CommandCursor. The
The :meth:`aggregate` method always returns an AsyncCommandCursor. The
pipeline argument must be a list.
.. seealso:: :doc:`/examples/aggregation`
@ -2958,7 +2958,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
"""Perform an aggregation and retrieve batches of raw BSON.
Similar to the :meth:`aggregate` method but returns a
:class:`~pymongo.cursor.AsyncRawBatchCursor`.
:class:`~pymongo.asynchronous.cursor.AsyncRawBatchCursor`.
This example demonstrates how to work with raw batches, but in practice
raw batches should be passed to an external library that can decode
@ -3014,14 +3014,14 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param new_name: new name for this collection
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: additional arguments to the rename command
may be passed as keyword arguments to this helper method
(i.e. ``dropTarget=True``)
.. note:: The :attr:`~pymongo.collection.AsyncCollection.write_concern` of
.. note:: The :attr:`~pymongo.asynchronous.collection.AsyncCollection.write_concern` of
this collection is automatically applied to this operation.
.. versionchanged:: 3.6
@ -3083,14 +3083,14 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:class:`~pymongo.collation.Collation`.
The :meth:`distinct` method obeys the :attr:`read_preference` of
this :class:`Collection`.
this :class:`AsyncCollection`.
:param key: name of the field for which we want to get the distinct
values
:param filter: A query document that specifies the documents
from which to retrieve the distinct values.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: See list of options above.
@ -3261,11 +3261,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
match the query, they are sorted and the first is deleted.
:param hint: An index to use to support the query predicate
specified either by its string name, or in the same format as
passed to :meth:`~pymongo.collection.AsyncCollection.create_index`
passed to :meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index`
(e.g. ``[('field', ASCENDING)]``). This option is only supported
on MongoDB 4.4 and above.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param let: Map of parameter names and values. Values must be
constant or closed expressions that do not reference document
fields. Parameters can then be accessed as variables in an
@ -3287,7 +3287,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
.. warning:: Starting in PyMongo 3.2, this command uses the
:class:`~pymongo.write_concern.WriteConcern` of this
:class:`~pymongo.collection.AsyncCollection` when connected to MongoDB >=
:class:`~pymongo.asynchronous.collection.AsyncCollection` when connected to MongoDB >=
3.2. Note that using an elevated write concern with this command may
be slower compared to using the default write concern.
@ -3359,11 +3359,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param hint: An index to use to support the query
predicate specified either by its string name, or in the same
format as passed to
:meth:`~pymongo.collection.AsyncCollection.create_index` (e.g.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.4 and above.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param let: Map of parameter names and values. Values must be
constant or closed expressions that do not reference document
fields. Parameters can then be accessed as variables in an
@ -3387,7 +3387,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
.. warning:: Starting in PyMongo 3.2, this command uses the
:class:`~pymongo.write_concern.WriteConcern` of this
:class:`~pymongo.collection.AsyncCollection` when connected to MongoDB >=
:class:`~pymongo.asynchronous.collection.AsyncCollection` when connected to MongoDB >=
3.2. Note that using an elevated write concern with this command may
be slower compared to using the default write concern.
@ -3461,7 +3461,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
The *upsert* option can be used to create the document if it doesn't
already exist.
>>> await db.example.delete_many({}).deleted_count
>>> (await db.example.delete_many({})).deleted_count
1
>>> await db.example.find_one_and_update(
... {'_id': 'userid'},
@ -3506,11 +3506,11 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
:param hint: An index to use to support the query
predicate specified either by its string name, or in the same
format as passed to
:meth:`~pymongo.collection.AsyncCollection.create_index` (e.g.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.4 and above.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param let: Map of parameter names and values. Values must be
constant or closed expressions that do not reference document
fields. Parameters can then be accessed as variables in an
@ -3534,7 +3534,7 @@ class AsyncCollection(common.BaseObject, Generic[_DocumentType]):
.. warning:: Starting in PyMongo 3.2, this command uses the
:class:`~pymongo.write_concern.WriteConcern` of this
:class:`~pymongo.collection.AsyncCollection` when connected to MongoDB >=
:class:`~pymongo.asynchronous.collection.AsyncCollection` when connected to MongoDB >=
3.2. Note that using an elevated write concern with this command may
be slower compared to using the default write concern.

View File

@ -189,7 +189,7 @@ class AsyncCommandCursor(Generic[_DocumentType]):
@property
def session(self) -> Optional[AsyncClientSession]:
"""The cursor's :class:`~pymongo.client_session.AsyncClientSession`, or None.
"""The cursor's :class:`~pymongo.asynchronous.client_session.AsyncClientSession`, or None.
.. versionadded:: 3.6
"""
@ -416,7 +416,7 @@ class AsyncRawBatchCommandCursor(AsyncCommandCursor[_DocumentType]):
"""Create a new cursor / iterator over raw batches of BSON data.
Should not be called directly by application developers -
see :meth:`~pymongo.collection.AsyncCollection.aggregate_raw_batches`
see :meth:`~pymongo.asynchronous.collection.AsyncCollection.aggregate_raw_batches`
instead.
.. seealso:: The MongoDB documentation on `cursors <https://dochub.mongodb.org/core/cursors>`_.

View File

@ -123,7 +123,7 @@ class AsyncCursor(Generic[_DocumentType]):
"""Create a new cursor.
Should not be called directly by application developers - see
:meth:`~pymongo.collection.AsyncCollection.find` instead.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find` instead.
.. seealso:: The MongoDB documentation on `cursors <https://dochub.mongodb.org/core/cursors>`_.
"""
@ -256,7 +256,7 @@ class AsyncCursor(Generic[_DocumentType]):
@property
def collection(self) -> AsyncCollection[_DocumentType]:
"""The :class:`~pymongo.collection.AsyncCollection` that this
"""The :class:`~pymongo.asynchronous.collection.AsyncCollection` that this
:class:`AsyncCursor` is iterating.
"""
return self._collection
@ -322,7 +322,7 @@ class AsyncCursor(Generic[_DocumentType]):
return base
def _clone_base(self, session: Optional[AsyncClientSession]) -> AsyncCursor:
"""Creates an empty Cursor object for information to be copied into."""
"""Creates an empty AsyncCursor object for information to be copied into."""
return self.__class__(self._collection, session=session)
def _query_spec(self) -> Mapping[str, Any]:
@ -527,7 +527,7 @@ class AsyncCursor(Generic[_DocumentType]):
def max_await_time_ms(self, max_await_time_ms: Optional[int]) -> AsyncCursor[_DocumentType]:
"""Specifies a time limit for a getMore operation on a
:attr:`~pymongo.cursor_shared.CursorType.TAILABLE_AWAIT` cursor. For all other
:attr:`~pymongo.cursor.CursorType.TAILABLE_AWAIT` cursor. For all other
types of cursor max_await_time_ms is ignored.
Raises :exc:`TypeError` if `max_await_time_ms` is not an integer or
@ -606,12 +606,12 @@ class AsyncCursor(Generic[_DocumentType]):
self._empty = False
if isinstance(index, slice):
if index.step is not None:
raise IndexError("Cursor instances do not support slice steps")
raise IndexError("AsyncCursor instances do not support slice steps")
skip = 0
if index.start is not None:
if index.start < 0:
raise IndexError("Cursor instances do not support negative indices")
raise IndexError("AsyncCursor instances do not support negative indices")
skip = index.start
if index.stop is not None:
@ -631,15 +631,15 @@ class AsyncCursor(Generic[_DocumentType]):
if isinstance(index, int):
if index < 0:
raise IndexError("Cursor instances do not support negative indices")
raise IndexError("AsyncCursor instances do not support negative indices")
clone = self.clone()
clone.skip(index + self._skip)
clone.limit(-1) # use a hard limit
clone._query_flags &= ~CursorType.TAILABLE_AWAIT # PYTHON-1371
for doc in clone: # type: ignore[attr-defined]
return doc
raise IndexError("no such item for Cursor instance")
raise TypeError("index %r cannot be applied to Cursor instances" % index)
raise IndexError("no such item for AsyncCursor instance")
raise TypeError("index %r cannot be applied to AsyncCursor instances" % index)
else:
raise IndexError("AsyncCursor does not support indexing")
@ -727,7 +727,7 @@ class AsyncCursor(Generic[_DocumentType]):
Text search results can be sorted by relevance::
cursor = await db.test.find(
cursor = db.test.find(
{'$text': {'$search': 'some words'}},
{'score': {'$meta': 'textScore'}})
@ -761,7 +761,7 @@ class AsyncCursor(Generic[_DocumentType]):
`explain command
<https://mongodb.com/docs/manual/reference/command/explain/>`_,
``allPlansExecution``. To use a different verbosity use
:meth:`~pymongo.database.AsyncDatabase.command` to run the explain
:meth:`~pymongo.asynchronous.database.AsyncDatabase.command` to run the explain
command directly.
.. seealso:: The MongoDB documentation on `explain <https://dochub.mongodb.org/core/explain>`_.
@ -796,7 +796,7 @@ class AsyncCursor(Generic[_DocumentType]):
already been used.
`index` should be an index as passed to
:meth:`~pymongo.collection.AsyncCollection.create_index`
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index`
(e.g. ``[('field', ASCENDING)]``) or the name of the index.
If `index` is ``None`` any existing hint for this query is
cleared. The last hint applied to this cursor takes precedence
@ -838,7 +838,7 @@ class AsyncCursor(Generic[_DocumentType]):
Raises :class:`TypeError` if `code` is not an instance of
:class:`str`. Raises :class:`~pymongo.errors.InvalidOperation` if this
:class:`Cursor` has already been used. Only the last call to
:class:`AsyncCursor` has already been used. Only the last call to
:meth:`where` applied to a :class:`AsyncCursor` has any effect.
.. note:: MongoDB 4.4 drops support for :class:`~bson.code.Code`
@ -936,7 +936,7 @@ class AsyncCursor(Generic[_DocumentType]):
@property
def session(self) -> Optional[AsyncClientSession]:
"""The cursor's :class:`~pymongo.client_session.AsyncClientSession`, or None.
"""The cursor's :class:`~pymongo.asynchronous.client_session.AsyncClientSession`, or None.
.. versionadded:: 3.6
"""
@ -1063,13 +1063,13 @@ class AsyncCursor(Generic[_DocumentType]):
:class:`str`.
The :meth:`distinct` method obeys the
:attr:`~pymongo.collection.AsyncCollection.read_preference` of the
:class:`~pymongo.collection.AsyncCollection` instance on which
:meth:`~pymongo.collection.AsyncCollection.find` was called.
:attr:`~pymongo.asynchronous.collection.AsyncCollection.read_preference` of the
:class:`~pymongo.asynchronous.collection.AsyncCollection` instance on which
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find` was called.
:param key: name of key for which we want to get the distinct values
.. seealso:: :meth:`pymongo.collection.AsyncCollection.distinct`
.. seealso:: :meth:`pymongo.asynchronous.collection.AsyncCollection.distinct`
"""
options: dict[str, Any] = {}
if self._spec:
@ -1111,7 +1111,7 @@ class AsyncCursor(Generic[_DocumentType]):
await self.close()
# If this is a tailable cursor the error is likely
# due to capped collection roll over. Setting
# self._killed to True ensures Cursor.alive will be
# self._killed to True ensures AsyncCursor.alive will be
# False. No need to re-raise.
if (
exc.code in _CURSOR_CLOSED_ERRORS
@ -1316,7 +1316,7 @@ class AsyncRawBatchCursor(AsyncCursor, Generic[_DocumentType]):
"""Create a new cursor / iterator over raw batches of BSON data.
Should not be called directly by application developers -
see :meth:`~pymongo.collection.AsyncCollection.find_raw_batches`
see :meth:`~pymongo.asynchronous.collection.AsyncCollection.find_raw_batches`
instead.
.. seealso:: The MongoDB documentation on `cursors <https://dochub.mongodb.org/core/cursors>`_.

View File

@ -74,7 +74,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
:class:`str`. Raises :class:`~pymongo.errors.InvalidName` if
`name` is not a valid database name.
:param client: A :class:`~pymongo.mongo_client.AsyncMongoClient` instance.
:param client: A :class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` instance.
:param name: The database name.
:param codec_options: An instance of
:class:`~bson.codec_options.CodecOptions`. If ``None`` (the
@ -102,8 +102,8 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
.. versionchanged:: 3.0
Added the codec_options, read_preference, and write_concern options.
:class:`~pymongo.database.AsyncDatabase` no longer returns an instance
of :class:`~pymongo.collection.AsyncCollection` for attribute names
:class:`~pymongo.asynchronous.database.AsyncDatabase` no longer returns an instance
of :class:`~pymongo.asynchronous.collection.AsyncCollection` for attribute names
with leading underscores. You must use dict-style lookups instead::
db['__my_collection__']
@ -230,10 +230,10 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> AsyncCollection[_DocumentType]:
"""Get a :class:`~pymongo.collection.AsyncCollection` with the given name
"""Get a :class:`~pymongo.asynchronous.collection.AsyncCollection` with the given name
and options.
Useful for creating a :class:`~pymongo.collection.AsyncCollection` with
Useful for creating a :class:`~pymongo.asynchronous.collection.AsyncCollection` with
different codec options, read preference, and/or write concern from
this :class:`AsyncDatabase`.
@ -307,7 +307,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
__iter__ = None
def __next__(self) -> NoReturn:
raise TypeError("'Database' object is not iterable")
raise TypeError("'AsyncDatabase' object is not iterable")
next = __next__
@ -364,7 +364,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
async for insert_change in stream:
print(insert_change)
except pymongo.errors.PyMongoError:
# The ChangeStream encountered an unrecoverable error or the
# The AsyncChangeStream encountered an unrecoverable error or the
# resume attempt failed to recreate the cursor.
logging.error("...")
@ -401,7 +401,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
the specified :class:`~bson.timestamp.Timestamp`. Requires
MongoDB >= 4.0.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param start_after: The same as `resume_after` except that
`start_after` can resume notifications after an invalidate event.
This option and `resume_after` are mutually exclusive.
@ -461,7 +461,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
check_exists: Optional[bool] = True,
**kwargs: Any,
) -> AsyncCollection[_DocumentType]:
"""Create a new :class:`~pymongo.collection.AsyncCollection` in this
"""Create a new :class:`~pymongo.asynchronous.collection.AsyncCollection` in this
database.
Normally collection creation is automatic. This method should
@ -488,7 +488,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
:param collation: An instance of
:class:`~pymongo.collation.Collation`.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param check_exists: if True (the default), send a listCollections command to
check if the collection already exists before creation.
:param kwargs: additional keyword arguments will
@ -621,19 +621,19 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
print(operation)
The :meth:`aggregate` method obeys the :attr:`read_preference` of this
:class:`Database`, except when ``$out`` or ``$merge`` are used, in
:class:`AsyncDatabase`, except when ``$out`` or ``$merge`` are used, in
which case :attr:`~pymongo.read_preferences.ReadPreference.PRIMARY`
is used.
.. note:: This method does not support the 'explain' option. Please
use :meth:`~pymongo.database.Database.command` instead.
use :meth:`~pymongo.asynchronous.database.AsyncDatabase.command` instead.
.. note:: The :attr:`~pymongo.database.AsyncDatabase.write_concern` of
.. note:: The :attr:`~pymongo.asynchronous.database.AsyncDatabase.write_concern` of
this collection is automatically applied to this operation.
:param pipeline: a list of aggregation pipeline stages
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param kwargs: extra `aggregate command`_ parameters.
All optional `aggregate command`_ parameters should be passed as
@ -656,7 +656,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
aggregate expression context (e.g. ``"$$var"``). This option is
only supported on MongoDB >= 5.0.
:return: A :class:`~pymongo.command_cursor.AsyncCommandCursor` over the result
:return: A :class:`~pymongo.asynchronous.command_cursor.AsyncCommandCursor` over the result
set.
.. versionadded:: 3.9
@ -851,7 +851,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
:param codec_options: A :class:`~bson.codec_options.CodecOptions`
instance.
:param session: A
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: additional keyword arguments will
@ -952,7 +952,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
:param codec_options: A :class:`~bson.codec_options.CodecOptions`
instance.
:param session: A
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to future getMores for this
command.
:param max_await_time_ms: The number of ms to wait for more data on future getMores for this command.
@ -1082,7 +1082,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
"""Get a cursor over the collections of this database.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param filter: A query document to filter the list of
collections returned from the listCollections command.
:param comment: A user-provided comment to attach to this
@ -1094,7 +1094,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
options differ by server version.
:return: An instance of :class:`~pymongo.command_cursor.AsyncCommandCursor`.
:return: An instance of :class:`~pymongo.asynchronous.command_cursor.AsyncCommandCursor`.
.. versionadded:: 3.6
"""
@ -1128,7 +1128,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
"""Get a cursor over the collections of this database.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param filter: A query document to filter the list of
collections returned from the listCollections command.
:param comment: A user-provided comment to attach to this
@ -1140,7 +1140,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
options differ by server version.
:return: An instance of :class:`~pymongo.command_cursor.AsyncCommandCursor`.
:return: An instance of :class:`~pymongo.asynchronous.command_cursor.AsyncCommandCursor`.
.. versionadded:: 3.6
"""
@ -1186,7 +1186,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
db.list_collection_names(filter=filter)
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param filter: A query document to filter the list of
collections returned from the listCollections command.
:param comment: A user-provided comment to attach to this
@ -1235,7 +1235,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
:param name_or_collection: the name of a collection to drop or the
collection object itself
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param encrypted_fields: **(BETA)** Document that describes the encrypted fields for
@ -1261,7 +1261,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
}
.. note:: The :attr:`~pymongo.database.Database.write_concern` of
.. note:: The :attr:`~pymongo.asynchronous.database.AsyncDatabase.write_concern` of
this database is automatically applied to this operation.
.. versionchanged:: 4.2
@ -1325,7 +1325,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
of the structure of the collection and the individual
documents.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param background: A boolean flag that determines whether
the command runs in the background. Requires MongoDB 4.4+.
:param comment: A user-provided comment to attach to this
@ -1347,7 +1347,7 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
name = name.name
if not isinstance(name, str):
raise TypeError("name_or_collection must be an instance of str or Collection")
raise TypeError("name_or_collection must be an instance of str or AsyncCollection")
cmd = {"validate": name, "scandata": scandata, "full": full}
if comment is not None:
cmd["comment"] = comment
@ -1400,12 +1400,12 @@ class AsyncDatabase(common.BaseObject, Generic[_DocumentType]):
:param dbref: the reference
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: any additional keyword arguments
are the same as the arguments to
:meth:`~pymongo.collection.AsyncCollection.find`.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find`.
.. versionchanged:: 4.1

View File

@ -673,7 +673,7 @@ class ClientEncryption(Generic[_DocumentType]):
All optional `create collection command`_ parameters should be passed
as keyword arguments to this method.
See the documentation for :meth:`~pymongo.database.AsyncDatabase.create_collection` for all valid options.
See the documentation for :meth:`~pymongo.asynchronous.database.AsyncDatabase.create_collection` for all valid options.
:raises: - :class:`~pymongo.errors.EncryptedCollectionError`: When either data-key creation or creating the collection fails.
@ -978,7 +978,7 @@ class ClientEncryption(Generic[_DocumentType]):
def get_keys(self) -> AsyncCursor[RawBSONDocument]:
"""Get all of the data keys.
:return: An instance of :class:`~pymongo.cursor.Cursor` over the data key
:return: An instance of :class:`~pymongo.asynchronous.cursor.AsyncCursor` over the data key
documents.
.. versionadded:: 4.2

View File

@ -17,18 +17,18 @@
.. seealso:: :doc:`/examples/high_availability` for examples of connecting
to replica sets or sets of mongos servers.
To get a :class:`~pymongo.database.Database` instance from a
:class:`MongoClient` use either dictionary-style or attribute-style
To get a :class:`~pymongo.asynchronous.database.AsyncDatabase` instance from a
:class:`AsyncMongoClient` use either dictionary-style or attribute-style
access:
.. doctest::
>>> from pymongo import MongoClient
>>> c = MongoClient()
>>> from pymongo import AsyncMongoClient
>>> c = AsyncMongoClient()
>>> c.test_database
Database(MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True), 'test_database')
AsyncDatabase(AsyncMongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True), 'test_database')
>>> c["test-database"]
Database(MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True), 'test-database')
AsyncDatabase(AsyncMongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True), 'test-database')
"""
from __future__ import annotations
@ -61,6 +61,7 @@ from bson.timestamp import Timestamp
from pymongo import _csot, common, helpers_shared, uri_parser
from pymongo.asynchronous import client_session, database, periodic_executor
from pymongo.asynchronous.change_stream import AsyncChangeStream, AsyncClusterChangeStream
from pymongo.asynchronous.client_bulk import _AsyncClientBulk
from pymongo.asynchronous.client_session import _EmptyServerSession
from pymongo.asynchronous.command_cursor import AsyncCommandCursor
from pymongo.asynchronous.settings import TopologySettings
@ -69,6 +70,7 @@ from pymongo.client_options import ClientOptions
from pymongo.errors import (
AutoReconnect,
BulkWriteError,
ClientBulkWriteException,
ConfigurationError,
ConnectionFailure,
InvalidOperation,
@ -83,8 +85,17 @@ from pymongo.lock import _HAS_REGISTER_AT_FORK, _ALock, _create_lock, _release_l
from pymongo.logger import _CLIENT_LOGGER, _log_or_warn
from pymongo.message import _CursorAddress, _GetMore, _Query
from pymongo.monitoring import ConnectionClosedReason
from pymongo.operations import _Op
from pymongo.operations import (
DeleteMany,
DeleteOne,
InsertOne,
ReplaceOne,
UpdateMany,
UpdateOne,
_Op,
)
from pymongo.read_preferences import ReadPreference, _ServerMode
from pymongo.results import ClientBulkWriteResult
from pymongo.server_selectors import writable_server_selector
from pymongo.server_type import SERVER_TYPE
from pymongo.topology_description import TOPOLOGY_TYPE, TopologyDescription
@ -130,6 +141,15 @@ _ReadCall = Callable[
_IS_SYNC = False
_WriteOp = Union[
InsertOne,
DeleteOne,
DeleteMany,
ReplaceOne,
UpdateOne,
UpdateMany,
]
class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
HOST = "localhost"
@ -175,18 +195,18 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
uri = "mongodb://%s:%s@%s" % (
quote_plus(user), quote_plus(password), host)
client = MongoClient(uri)
client = AsyncMongoClient(uri)
Unix domain sockets are also supported. The socket path must be percent
encoded in the URI::
uri = "mongodb://%s:%s@%s" % (
quote_plus(user), quote_plus(password), quote_plus(socket_path))
client = MongoClient(uri)
client = AsyncMongoClient(uri)
But not when passed as a simple hostname::
client = MongoClient('/tmp/mongodb-27017.sock')
client = AsyncMongoClient('/tmp/mongodb-27017.sock')
Starting with version 3.6, PyMongo supports mongodb+srv:// URIs. The
URI must include one, and only one, hostname. The hostname will be
@ -202,10 +222,10 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
for more details. Note that the use of SRV URIs implicitly enables
TLS support. Pass tls=false in the URI to override.
.. note:: MongoClient creation will block waiting for answers from
.. note:: AsyncMongoClient creation will block waiting for answers from
DNS when mongodb+srv:// URIs are used.
.. note:: Starting with version 3.0 the :class:`MongoClient`
.. note:: Starting with version 3.0 the :class:`AsyncMongoClient`
constructor no longer blocks while connecting to the server or
servers, and it no longer raises
:class:`~pymongo.errors.ConnectionFailure` if they are
@ -216,7 +236,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
like this::
from pymongo.errors import ConnectionFailure
client = MongoClient()
client = AsyncMongoClient()
try:
# The ping command is cheap and does not require auth.
client.admin.command('ping')
@ -242,7 +262,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
documents returned from queries on this client
:param tz_aware: if ``True``,
:class:`~datetime.datetime` instances returned as values
in a document by this :class:`MongoClient` will be timezone
in a document by this :class:`AsyncMongoClient` will be timezone
aware (otherwise they will be naive)
:param connect: **Not supported by AsyncMongoClient**.
:param type_registry: instance of
@ -314,7 +334,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
- `serverMonitoringMode`: (optional) The server monitoring mode to use.
Valid values are the strings: "auto", "stream", "poll". Defaults to "auto".
- `appname`: (string or None) The name of the application that
created this MongoClient instance. The server will log this value
created this AsyncMongoClient instance. The server will log this value
upon establishing each connection. It is also recorded in the slow
query log and profile collections.
- `driver`: (pair or None) A driver implemented on top of PyMongo can
@ -324,47 +344,47 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
- `event_listeners`: a list or tuple of event listeners. See
:mod:`~pymongo.monitoring` for details.
- `retryWrites`: (boolean) Whether supported write operations
executed within this MongoClient will be retried once after a
executed within this AsyncMongoClient will be retried once after a
network error. Defaults to ``True``.
The supported write operations are:
- :meth:`~pymongo.collection.Collection.bulk_write`, as long as
:class:`~pymongo.operations.UpdateMany` or
:class:`~pymongo.operations.DeleteMany` are not included.
- :meth:`~pymongo.collection.Collection.delete_one`
- :meth:`~pymongo.collection.Collection.insert_one`
- :meth:`~pymongo.collection.Collection.insert_many`
- :meth:`~pymongo.collection.Collection.replace_one`
- :meth:`~pymongo.collection.Collection.update_one`
- :meth:`~pymongo.collection.Collection.find_one_and_delete`
- :meth:`~pymongo.collection.Collection.find_one_and_replace`
- :meth:`~pymongo.collection.Collection.find_one_and_update`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write`, as long as
:class:`~pymongo.asynchronous.operations.UpdateMany` or
:class:`~pymongo.asynchronous.operations.DeleteMany` are not included.
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.delete_one`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.insert_one`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.insert_many`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.replace_one`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.update_one`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one_and_delete`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one_and_replace`
- :meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one_and_update`
Unsupported write operations include, but are not limited to,
:meth:`~pymongo.collection.Collection.aggregate` using the ``$out``
:meth:`~pymongo.asynchronous.collection.AsyncCollection.aggregate` using the ``$out``
pipeline operator and any operation with an unacknowledged write
concern (e.g. {w: 0})). See
https://github.com/mongodb/specifications/blob/master/source/retryable-writes/retryable-writes.rst
- `retryReads`: (boolean) Whether supported read operations
executed within this MongoClient will be retried once after a
executed within this AsyncMongoClient will be retried once after a
network error. Defaults to ``True``.
The supported read operations are:
:meth:`~pymongo.collection.Collection.find`,
:meth:`~pymongo.collection.Collection.find_one`,
:meth:`~pymongo.collection.Collection.aggregate` without ``$out``,
:meth:`~pymongo.collection.Collection.distinct`,
:meth:`~pymongo.collection.Collection.count`,
:meth:`~pymongo.collection.Collection.estimated_document_count`,
:meth:`~pymongo.collection.Collection.count_documents`,
:meth:`pymongo.collection.Collection.watch`,
:meth:`~pymongo.collection.Collection.list_indexes`,
:meth:`pymongo.database.Database.watch`,
:meth:`~pymongo.database.Database.list_collections`,
:meth:`pymongo.mongo_client.MongoClient.watch`,
and :meth:`~pymongo.mongo_client.MongoClient.list_databases`.
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find`,
:meth:`~pymongo.asynchronous.collection.AsyncCollection.find_one`,
:meth:`~pymongo.asynchronous.collection.AsyncCollection.aggregate` without ``$out``,
:meth:`~pymongo.asynchronous.collection.AsyncCollection.distinct`,
:meth:`~pymongo.asynchronous.collection.AsyncCollection.count`,
:meth:`~pymongo.asynchronous.collection.AsyncCollection.estimated_document_count`,
:meth:`~pymongo.asynchronous.collection.AsyncCollection.count_documents`,
:meth:`pymongo.asynchronous.collection.AsyncCollection.watch`,
:meth:`~pymongo.asynchronous.collection.AsyncCollection.list_indexes`,
:meth:`pymongo.asynchronous.database.AsyncDatabase.watch`,
:meth:`~pymongo.asynchronous.database.AsyncDatabase.list_collections`,
:meth:`pymongo.asynchronous.mongo_client.AsyncMongoClient.watch`,
and :meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.list_databases`.
Unsupported read operations include, but are not limited to
:meth:`~pymongo.database.Database.command` and any getMore
:meth:`~pymongo.asynchronous.database.AsyncDatabase.command` and any getMore
operation on a cursor.
Enabling retryable reads makes applications more resilient to
@ -403,7 +423,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
- `srvServiceName`: (string) The SRV service name to use for
"mongodb+srv://" URIs. Defaults to "mongodb". Use it like so::
MongoClient("mongodb+srv://example.com/?srvServiceName=customname")
AsyncMongoClient("mongodb+srv://example.com/?srvServiceName=customname")
- `srvMaxHosts`: (int) limits the number of mongos-like hosts a client will
connect to. More specifically, when a "mongodb+srv://" connection string
resolves to more than srvMaxHosts number of hosts, the client will randomly
@ -470,7 +490,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
this example, both the space and slash special characters are passed
as-is::
MongoClient(username="user name", password="pass/word")
AsyncMongoClient(username="user name", password="pass/word")
- `authSource`: The database to authenticate on. Defaults to the
database specified in the URI, if provided, or to "admin".
@ -549,9 +569,9 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
configures this client to automatically encrypt collection commands
and automatically decrypt results. See
:ref:`automatic-client-side-encryption` for an example.
If a :class:`MongoClient` is configured with
If a :class:`AsyncMongoClient` is configured with
``auto_encryption_opts`` and a non-None ``maxPoolSize``, a
separate internal ``MongoClient`` is created if any of the
separate internal ``AsyncMongoClient`` is created if any of the
following are true:
- A ``key_vault_client`` is not passed to
@ -640,14 +660,14 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
``socketKeepAlive`` now defaults to ``True``.
.. versionchanged:: 3.0
:class:`~pymongo.mongo_client.MongoClient` is now the one and only
:class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` is now the one and only
client class for a standalone server, mongos, or replica set.
It includes the functionality that had been split into
:class:`~pymongo.mongo_client.MongoReplicaSetClient`: it can connect
:class:`~pymongo.asynchronous.mongo_client.MongoReplicaSetClient`: it can connect
to a replica set, discover all its members, and monitor the set for
stepdowns, elections, and reconfigs.
The :class:`~pymongo.mongo_client.MongoClient` constructor no
The :class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` constructor no
longer blocks while connecting to the server or servers, and it no
longer raises :class:`~pymongo.errors.ConnectionFailure` if they
are unavailable, nor :class:`~pymongo.errors.ConfigurationError`
@ -659,10 +679,10 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
provides meaningful information; even if the client is disconnected,
it may discover a server in time to fulfill the next operation.
In PyMongo 2.x, :class:`~pymongo.MongoClient` accepted a list of
In PyMongo 2.x, :class:`~pymongo.asynchronous.AsyncMongoClient` accepted a list of
standalone MongoDB servers and used the first it could connect to::
MongoClient(['host1.com:27017', 'host2.com:27017'])
AsyncMongoClient(['host1.com:27017', 'host2.com:27017'])
A list of multiple standalones is no longer supported; if multiple
servers are listed they must be members of the same replica set, or
@ -685,11 +705,11 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
The ``copy_database`` method is removed, see the
:doc:`copy_database examples </examples/copydb>` for alternatives.
The :meth:`MongoClient.disconnect` method is removed; it was a
synonym for :meth:`~pymongo.MongoClient.close`.
The :meth:`AsyncMongoClient.disconnect` method is removed; it was a
synonym for :meth:`~pymongo.asynchronous.AsyncMongoClient.close`.
:class:`~pymongo.mongo_client.MongoClient` no longer returns an
instance of :class:`~pymongo.database.Database` for attribute names
:class:`~pymongo.asynchronous.mongo_client.AsyncMongoClient` no longer returns an
instance of :class:`~pymongo.asynchronous.database.AsyncDatabase` for attribute names
with leading underscores. You must use dict-style lookups instead::
client['__my_database__']
@ -924,7 +944,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
Performs an aggregation with an implicit initial ``$changeStream``
stage and returns a
:class:`~pymongo.change_stream.ClusterChangeStream` cursor which
:class:`~pymongo.asynchronous.change_stream.AsyncClusterChangeStream` cursor which
iterates over changes on all databases on this cluster.
Introduced in MongoDB 4.0.
@ -935,10 +955,10 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
for change in stream:
print(change)
The :class:`~pymongo.change_stream.ClusterChangeStream` iterable
The :class:`~pymongo.asynchronous.change_stream.AsyncClusterChangeStream` iterable
blocks until the next change document is returned or an error is
raised. If the
:meth:`~pymongo.change_stream.ClusterChangeStream.next` method
:meth:`~pymongo.asynchronous.change_stream.AsyncClusterChangeStream.next` method
encounters a network error when retrieving a batch from the server,
it will automatically attempt to recreate the cursor such that no
change events are missed. Any error encountered during the resume
@ -951,7 +971,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
for insert_change in stream:
print(insert_change)
except pymongo.errors.PyMongoError:
# The ChangeStream encountered an unrecoverable error or the
# The AsyncChangeStream encountered an unrecoverable error or the
# resume attempt failed to recreate the cursor.
logging.error("...")
@ -988,7 +1008,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
the specified :class:`~bson.timestamp.Timestamp`. Requires
MongoDB >= 4.0.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param start_after: The same as `resume_after` except that
`start_after` can resume notifications after an invalidate event.
This option and `resume_after` are mutually exclusive.
@ -996,7 +1016,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
command.
:param show_expanded_events: Include expanded events such as DDL events like `dropIndexes`.
:return: A :class:`~pymongo.change_stream.ClusterChangeStream` cursor.
:return: A :class:`~pymongo.asynchronous.change_stream.AsyncClusterChangeStream` cursor.
.. versionchanged:: 4.3
Added `show_expanded_events` parameter.
@ -1062,9 +1082,9 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
"""Set of all currently connected servers.
.. warning:: When connected to a replica set the value of :attr:`nodes`
can change over time as :class:`MongoClient`'s view of the replica
can change over time as :class:`AsyncMongoClient`'s view of the replica
set changes. :attr:`nodes` can also be an empty set when
:class:`MongoClient` is first instantiated and hasn't yet connected
:class:`AsyncMongoClient` is first instantiated and hasn't yet connected
to any servers, or a network partition causes it to lose connection
to all servers.
"""
@ -1176,16 +1196,16 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
"""Start a logical session.
This method takes the same parameters as
:class:`~pymongo.client_session.SessionOptions`. See the
:mod:`~pymongo.client_session` module for details and examples.
:class:`~pymongo.asynchronous.client_session.SessionOptions`. See the
:mod:`~pymongo.asynchronous.client_session` module for details and examples.
A :class:`~pymongo.client_session.AsyncClientSession` may only be used with
the MongoClient that started it. :class:`AsyncClientSession` instances are
A :class:`~pymongo.asynchronous.client_session.AsyncClientSession` may only be used with
the AsyncMongoClient that started it. :class:`AsyncClientSession` instances are
**not thread-safe or fork-safe**. They can only be used by one thread
or process at a time. A single :class:`AsyncClientSession` cannot be used
to run multiple operations concurrently.
:return: An instance of :class:`~pymongo.client_session.AsyncClientSession`.
:return: An instance of :class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
.. versionadded:: 3.6
"""
@ -1237,7 +1257,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
"""Get the database named in the MongoDB connection URI.
>>> uri = 'mongodb://host/my_database'
>>> client = MongoClient(uri)
>>> client = AsyncMongoClient(uri)
>>> db = client.get_default_database()
>>> assert db.name == 'my_database'
>>> db = client.get_database()
@ -1250,19 +1270,19 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
was provided in the URI.
:param codec_options: An instance of
:class:`~bson.codec_options.CodecOptions`. If ``None`` (the
default) the :attr:`codec_options` of this :class:`MongoClient` is
default) the :attr:`codec_options` of this :class:`AsyncMongoClient` is
used.
:param read_preference: The read preference to use. If
``None`` (the default) the :attr:`read_preference` of this
:class:`MongoClient` is used. See :mod:`~pymongo.read_preferences`
:class:`AsyncMongoClient` is used. See :mod:`~pymongo.read_preferences`
for options.
:param write_concern: An instance of
:class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the
default) the :attr:`write_concern` of this :class:`MongoClient` is
default) the :attr:`write_concern` of this :class:`AsyncMongoClient` is
used.
:param read_concern: An instance of
:class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the
default) the :attr:`read_concern` of this :class:`MongoClient` is
default) the :attr:`read_concern` of this :class:`AsyncMongoClient` is
used.
:param comment: A user-provided comment to attach to this
command.
@ -1294,12 +1314,12 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
write_concern: Optional[WriteConcern] = None,
read_concern: Optional[ReadConcern] = None,
) -> database.AsyncDatabase[_DocumentType]:
"""Get a :class:`~pymongo.database.Database` with the given name and
"""Get a :class:`~pymongo.asynchronous.database.AsyncDatabase` with the given name and
options.
Useful for creating a :class:`~pymongo.database.Database` with
Useful for creating a :class:`~pymongo.asynchronous.database.AsyncDatabase` with
different codec options, read preference, and/or write concern from
this :class:`MongoClient`.
this :class:`AsyncMongoClient`.
>>> client.read_preference
Primary()
@ -1317,19 +1337,19 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
returned.
:param codec_options: An instance of
:class:`~bson.codec_options.CodecOptions`. If ``None`` (the
default) the :attr:`codec_options` of this :class:`MongoClient` is
default) the :attr:`codec_options` of this :class:`AsyncMongoClient` is
used.
:param read_preference: The read preference to use. If
``None`` (the default) the :attr:`read_preference` of this
:class:`MongoClient` is used. See :mod:`~pymongo.read_preferences`
:class:`AsyncMongoClient` is used. See :mod:`~pymongo.read_preferences`
for options.
:param write_concern: An instance of
:class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the
default) the :attr:`write_concern` of this :class:`MongoClient` is
default) the :attr:`write_concern` of this :class:`AsyncMongoClient` is
used.
:param read_concern: An instance of
:class:`~pymongo.read_concern.ReadConcern`. If ``None`` (the
default) the :attr:`read_concern` of this :class:`MongoClient` is
default) the :attr:`read_concern` of this :class:`AsyncMongoClient` is
used.
.. versionchanged:: 3.5
@ -1346,7 +1366,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
)
def _database_default_options(self, name: str) -> database.AsyncDatabase:
"""Get a Database instance with the default settings."""
"""Get a AsyncDatabase instance with the default settings."""
return self.get_database(
name,
codec_options=DEFAULT_CODEC_OPTIONS,
@ -1426,7 +1446,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
`replicaSet` option.
.. versionadded:: 3.0
MongoClient gained this property in version 3.0.
AsyncMongoClient gained this property in version 3.0.
"""
return await self._topology.get_primary() # type: ignore[return-value]
@ -1439,7 +1459,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
client was created without the `replicaSet` option.
.. versionadded:: 3.0
MongoClient gained this property in version 3.0.
AsyncMongoClient gained this property in version 3.0.
"""
return await self._topology.get_secondaries()
@ -1522,7 +1542,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
await self._encrypter.close()
async def _get_topology(self) -> Topology:
"""Get the internal :class:`~pymongo.topology.Topology` object.
"""Get the internal :class:`~pymongo.asynchronous.topology.Topology` object.
If this client was created with "connect=False", calling _get_topology
launches the connection process in the background.
@ -1720,7 +1740,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
retryable: bool,
func: _WriteCall[T],
session: Optional[AsyncClientSession],
bulk: Optional[_AsyncBulk],
bulk: Optional[Union[_AsyncBulk, _AsyncClientBulk]],
operation: str,
operation_id: Optional[int] = None,
) -> T:
@ -1750,7 +1770,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
self,
func: _WriteCall[T] | _ReadCall[T],
session: Optional[AsyncClientSession],
bulk: Optional[_AsyncBulk],
bulk: Optional[Union[_AsyncBulk, _AsyncClientBulk]],
operation: str,
is_read: bool = False,
address: Optional[_Address] = None,
@ -1833,7 +1853,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
func: _WriteCall[T],
session: Optional[AsyncClientSession],
operation: str,
bulk: Optional[_AsyncBulk] = None,
bulk: Optional[Union[_AsyncBulk, _AsyncClientBulk]] = None,
operation_id: Optional[int] = None,
) -> T:
"""Execute an operation with consecutive retries if possible
@ -2074,7 +2094,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
"""Get information about the MongoDB server we're connected to.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
.. versionchanged:: 3.6
Added ``session`` parameter.
@ -2117,7 +2137,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
"""Get a cursor over the databases of the connected server.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
:param kwargs: Optional parameters of the
@ -2127,7 +2147,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
options differ by server version.
:return: An instance of :class:`~pymongo.command_cursor.CommandCursor`.
:return: An instance of :class:`~pymongo.asynchronous.command_cursor.AsyncCommandCursor`.
.. versionadded:: 3.6
"""
@ -2141,7 +2161,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
"""Get a list of the names of all databases on the connected server.
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
@ -2163,13 +2183,13 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
"""Drop a database.
Raises :class:`TypeError` if `name_or_database` is not an instance of
:class:`str` or :class:`~pymongo.database.Database`.
:class:`str` or :class:`~pymongo.asynchronous.database.AsyncDatabase`.
:param name_or_database: the name of a database to drop, or a
:class:`~pymongo.database.Database` instance representing the
:class:`~pymongo.asynchronous.database.AsyncDatabase` instance representing the
database to drop
:param session: a
:class:`~pymongo.client_session.AsyncClientSession`.
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param comment: A user-provided comment to attach to this
command.
@ -2179,7 +2199,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
.. versionchanged:: 3.6
Added ``session`` parameter.
.. note:: The :attr:`~pymongo.mongo_client.MongoClient.write_concern` of
.. note:: The :attr:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.write_concern` of
this client is automatically applied to this operation.
.. versionchanged:: 3.4
@ -2192,7 +2212,7 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
name = name.name
if not isinstance(name, str):
raise TypeError("name_or_database must be an instance of str or a Database")
raise TypeError("name_or_database must be an instance of str or a AsyncDatabase")
async with await self._conn_for_writes(session, operation=_Op.DROP_DATABASE) as conn:
await self[name]._command(
@ -2204,10 +2224,134 @@ class AsyncMongoClient(common.BaseObject, Generic[_DocumentType]):
session=session,
)
@_csot.apply
async def bulk_write(
self,
models: Sequence[_WriteOp[_DocumentType]],
session: Optional[AsyncClientSession] = None,
ordered: bool = True,
verbose_results: bool = False,
bypass_document_validation: Optional[bool] = None,
comment: Optional[Any] = None,
let: Optional[Mapping] = None,
write_concern: Optional[WriteConcern] = None,
) -> ClientBulkWriteResult:
"""Send a batch of write operations, potentially across multiple namespaces, to the server.
Requests are passed as a list of write operation instances (
:class:`~pymongo.operations.InsertOne`,
:class:`~pymongo.operations.UpdateOne`,
:class:`~pymongo.operations.UpdateMany`,
:class:`~pymongo.operations.ReplaceOne`,
:class:`~pymongo.operations.DeleteOne`, or
:class:`~pymongo.operations.DeleteMany`).
>>> async for doc in db.test.find({}):
... print(doc)
...
{'x': 1, '_id': ObjectId('54f62e60fba5226811f634ef')}
{'x': 1, '_id': ObjectId('54f62e60fba5226811f634f0')}
...
>>> async for doc in db.coll.find({}):
... print(doc)
...
{'x': 2, '_id': ObjectId('507f1f77bcf86cd799439011')}
...
>>> # DeleteMany, UpdateOne, and UpdateMany are also available.
>>> from pymongo import InsertOne, DeleteOne, ReplaceOne
>>> models = [InsertOne(namespace="db.test", document={'y': 1}),
... DeleteOne(namespace="db.test", filter={'x': 1}),
... InsertOne(namespace="db.coll", document={'y': 2}),
... ReplaceOne(namespace="db.test", filter={'w': 1}, replacement={'z': 1}, upsert=True)]
>>> result = await client.bulk_write(models=models)
>>> result.inserted_count
2
>>> result.deleted_count
1
>>> result.modified_count
0
>>> result.upserted_ids
{3: ObjectId('54f62ee28891e756a6e1abd5')}
>>> async for doc in db.test.find({}):
... print(doc)
...
{'x': 1, '_id': ObjectId('54f62e60fba5226811f634f0')}
{'y': 1, '_id': ObjectId('54f62ee2fba5226811f634f1')}
{'z': 1, '_id': ObjectId('54f62ee28891e756a6e1abd5')}
...
>>> async for doc in db.coll.find({}):
... print(doc)
...
{'x': 2, '_id': ObjectId('507f1f77bcf86cd799439011')}
{'y': 2, '_id': ObjectId('507f1f77bcf86cd799439012')}
:param models: A list of write operation instances.
:param session: (optional) An instance of
:class:`~pymongo.asynchronous.client_session.AsyncClientSession`.
:param ordered: If ``True`` (the default), requests will be
performed on the server serially, in the order provided. If an error
occurs all remaining operations are aborted. If ``False``, requests
will be still performed on the server serially, in the order provided,
but all operations will be attempted even if any errors occur.
:param verbose_results: If ``True``, detailed results for each
successful operation will be included in the returned
:class:`~pymongo.results.ClientBulkWriteResult`. Default is ``False``.
:param bypass_document_validation: (optional) If ``True``, allows the
write to opt-out of document level validation. Default is ``False``.
:param comment: (optional) A user-provided comment to attach to this
command.
:param let: (optional) Map of parameter names and values. Values must be
constant or closed expressions that do not reference document
fields. Parameters can then be accessed as variables in an
aggregate expression context (e.g. "$$var").
:param write_concern: (optional) The write concern to use for this bulk write.
:return: An instance of :class:`~pymongo.results.ClientBulkWriteResult`.
.. seealso:: :ref:`writes-and-ids`
.. note:: requires MongoDB server version 8.0+.
.. versionadded:: 4.9
"""
if self._options.auto_encryption_opts:
raise InvalidOperation(
"MongoClient.bulk_write does not currently support automatic encryption"
)
if session and session.in_transaction:
# Inherit the transaction write concern.
if write_concern:
raise InvalidOperation("Cannot set write concern after starting a transaction")
write_concern = session._transaction.opts.write_concern # type: ignore[union-attr]
else:
# Inherit the client's write concern if none is provided.
if not write_concern:
write_concern = self.write_concern
common.validate_list("models", models)
blk = _AsyncClientBulk(
self,
write_concern=write_concern, # type: ignore[arg-type]
ordered=ordered,
bypass_document_validation=bypass_document_validation,
comment=comment,
let=let,
verbose_results=verbose_results,
)
for model in models:
try:
model._add_to_client_bulk(blk)
except AttributeError:
raise TypeError(f"{model!r} is not a valid request") from None
return await blk.execute(session, _Op.BULK_WRITE)
def _retryable_error_doc(exc: PyMongoError) -> Optional[Mapping[str, Any]]:
"""Return the server response from PyMongo exception or None."""
if isinstance(exc, BulkWriteError):
if isinstance(exc, (BulkWriteError, ClientBulkWriteException)):
# Check the last writeConcernError to determine if this
# BulkWriteError is retryable.
wces = exc.details["writeConcernErrors"]
@ -2242,10 +2386,14 @@ def _add_retryable_write_error(exc: PyMongoError, max_wire_version: int, is_mong
# AsyncConnection errors are always retryable except NotPrimaryError and WaitQueueTimeoutError which is
# handled above.
if isinstance(exc, ConnectionFailure) and not isinstance(
exc, (NotPrimaryError, WaitQueueTimeoutError)
if isinstance(exc, ClientBulkWriteException):
exc_to_check = exc.error
else:
exc_to_check = exc
if isinstance(exc_to_check, ConnectionFailure) and not isinstance(
exc_to_check, (NotPrimaryError, WaitQueueTimeoutError)
):
exc._add_error_label("RetryableWriteError")
exc_to_check._add_error_label("RetryableWriteError")
class _MongoClientErrorHandler:
@ -2292,6 +2440,8 @@ class _MongoClientErrorHandler:
return
self.handled = True
if self.session:
if isinstance(exc_val, ClientBulkWriteException):
exc_val = exc_val.error
if isinstance(exc_val, ConnectionFailure):
if self.session.in_transaction:
exc_val._add_error_label("TransientTransactionError")
@ -2303,7 +2453,7 @@ class _MongoClientErrorHandler:
):
await self.session._unpin()
err_ctx = _ErrorContext(
exc_val,
exc_val, # type: ignore[arg-type]
self.max_wire_version,
self.sock_generation,
self.completed_handshake,
@ -2330,7 +2480,7 @@ class _ClientConnectionRetryable(Generic[T]):
self,
mongo_client: AsyncMongoClient,
func: _WriteCall[T] | _ReadCall[T],
bulk: Optional[_AsyncBulk],
bulk: Optional[Union[_AsyncBulk, _AsyncClientBulk]],
operation: str,
is_read: bool = False,
session: Optional[AsyncClientSession] = None,
@ -2407,7 +2557,10 @@ class _ClientConnectionRetryable(Generic[T]):
if not self._is_read:
if not self._retryable:
raise
retryable_write_error_exc = exc.has_error_label("RetryableWriteError")
if isinstance(exc, ClientBulkWriteException) and exc.error:
retryable_write_error_exc = exc.error.has_error_label("RetryableWriteError")
else:
retryable_write_error_exc = exc.has_error_label("RetryableWriteError")
if retryable_write_error_exc:
assert self._session
await self._session._unpin()

View File

@ -48,6 +48,15 @@ def _sanitize(error: Exception) -> None:
error.__cause__ = None
def _monotonic_duration(start: float) -> float:
"""Return the duration since the given start time.
Accounts for buggy platforms where time.monotonic() is not monotonic.
See PYTHON-4600.
"""
return max(0.0, time.monotonic() - start)
class MonitorBase:
def __init__(self, topology: Topology, name: str, interval: int, min_interval: float):
"""Base class to do periodic work on a background thread.
@ -247,7 +256,7 @@ class Monitor(MonitorBase):
_sanitize(error)
sd = self._server_description
address = sd.address
duration = time.monotonic() - start
duration = _monotonic_duration(start)
if self._publish:
awaited = bool(self._stream and sd.is_server_type_known and sd.topology_version)
assert self._listeners is not None
@ -317,7 +326,8 @@ class Monitor(MonitorBase):
else:
# New connection handshake or polling hello (MongoDB <4.4).
response = await conn._hello(cluster_time, None, None)
return response, time.monotonic() - start
duration = _monotonic_duration(start)
return response, duration
class SrvMonitor(MonitorBase):
@ -441,7 +451,7 @@ class _RttMonitor(MonitorBase):
raise Exception("_RttMonitor closed")
start = time.monotonic()
await conn.hello()
return time.monotonic() - start
return _monotonic_duration(start)
# Close monitors to cancel any in progress streaming checks before joining

View File

@ -645,6 +645,7 @@ class Topology:
:exc:`~.errors.InvalidOperation`.
"""
async with self._lock:
old_td = self._description
for server in self._servers.values():
await server.close()
@ -664,9 +665,30 @@ class Topology:
# Publish only after releasing the lock.
if self._publish_tp:
assert self._events is not None
self._description = TopologyDescription(
TOPOLOGY_TYPE.Unknown,
{},
self._description.replica_set_name,
self._description.max_set_version,
self._description.max_election_id,
self._description._topology_settings,
)
self._events.put(
(
self._listeners.publish_topology_description_changed,
(
old_td,
self._description,
self._topology_id,
),
)
)
self._events.put((self._listeners.publish_topology_closed, (self._topology_id,)))
if self._publish_server or self._publish_tp:
# Make sure the events executor thread is fully closed before publishing the remaining events
self.__events_executor.close()
self.__events_executor.join(1)
process_events_queue(weakref.ref(self._events)) # type: ignore[arg-type]
@property
def description(self) -> TopologyDescription:

View File

@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Any, Iterable, Mapping, Optional, Sequence, Un
from bson.errors import InvalidDocument
if TYPE_CHECKING:
from pymongo.results import ClientBulkWriteResult
from pymongo.typings import _DocumentOut
@ -308,6 +309,62 @@ class BulkWriteError(OperationFailure):
return False
class ClientBulkWriteException(OperationFailure):
"""Exception class for client-level bulk write errors."""
details: _DocumentOut
verbose: bool
def __init__(self, results: _DocumentOut, verbose: bool) -> None:
super().__init__("batch op errors occurred", 65, results)
self.verbose = verbose
def __reduce__(self) -> tuple[Any, Any]:
return self.__class__, (self.details,)
@property
def error(self) -> Optional[Any]:
"""A top-level error that occurred when attempting to
communicate with the server or execute the bulk write.
This value may not be populated if the exception was
thrown due to errors occurring on individual writes.
"""
return self.details.get("error", None)
@property
def write_concern_errors(self) -> Optional[list[WriteConcernError]]:
"""Write concern errors that occurred during the bulk write.
This list may have multiple items if more than one
server command was required to execute the bulk write.
"""
return self.details.get("writeConcernErrors", [])
@property
def write_errors(self) -> Optional[Mapping[int, WriteError]]:
"""Errors that occurred during the execution of individual write operations.
This map will contain at most one entry if the bulk write was ordered.
"""
return self.details.get("writeErrors", {})
@property
def partial_result(self) -> Optional[ClientBulkWriteResult]:
"""The results of any successful operations that were
performed before the error was encountered.
"""
from pymongo.results import ClientBulkWriteResult
if self.details.get("anySuccessful"):
return ClientBulkWriteResult(
self.details, # type: ignore[arg-type]
acknowledged=True,
has_verbose_results=self.verbose,
)
return None
class InvalidOperation(PyMongoError):
"""Raised when a client attempts to perform an invalid operation."""

View File

@ -21,6 +21,7 @@ MongoDB.
"""
from __future__ import annotations
import copy
import datetime
import random
import struct
@ -101,7 +102,12 @@ _OP_MAP = {
_UPDATE: b"\x04updates\x00\x00\x00\x00\x00",
_DELETE: b"\x04deletes\x00\x00\x00\x00\x00",
}
_FIELD_MAP = {"insert": "documents", "update": "updates", "delete": "deletes"}
_FIELD_MAP = {
"insert": "documents",
"update": "updates",
"delete": "deletes",
"bulkWrite": "bulkWrite",
}
_UNICODE_REPLACE_CODEC_OPTIONS: CodecOptions[Mapping[str, Any]] = CodecOptions(
unicode_decode_error_handler="replace"
@ -136,6 +142,17 @@ def _convert_exception(exception: Exception) -> dict[str, Any]:
return {"errmsg": str(exception), "errtype": exception.__class__.__name__}
def _convert_client_bulk_exception(exception: Exception) -> dict[str, Any]:
"""Convert an Exception into a failure document for publishing,
for use in client-level bulk write API.
"""
return {
"errmsg": str(exception),
"code": exception.code, # type: ignore[attr-defined]
"errtype": exception.__class__.__name__,
}
def _convert_write_result(
operation: str, command: Mapping[str, Any], result: Mapping[str, Any]
) -> dict[str, Any]:
@ -551,8 +568,8 @@ _OP_MSG_MAP = {
}
class _BulkWriteContext:
"""A wrapper around AsyncConnection for use with write splitting functions."""
class _BulkWriteContextBase:
"""Private base class for wrapping around AsyncConnection to use with write splitting functions."""
__slots__ = (
"db_name",
@ -576,7 +593,7 @@ class _BulkWriteContext:
conn: _AgnosticConnection,
operation_id: int,
listeners: _EventListeners,
session: _AgnosticClientSession,
session: Optional[_AgnosticClientSession],
op_type: int,
codec: CodecOptions,
):
@ -593,17 +610,6 @@ class _BulkWriteContext:
self.op_type = op_type
self.codec = codec
def batch_command(
self, cmd: MutableMapping[str, Any], docs: list[Mapping[str, Any]]
) -> tuple[int, Union[bytes, dict[str, Any]], list[Mapping[str, Any]]]:
namespace = self.db_name + ".$cmd"
request_id, msg, to_send = _do_batched_op_msg(
namespace, self.op_type, cmd, docs, self.codec, self
)
if not to_send:
raise InvalidOperation("cannot do an empty bulk write")
return request_id, msg, to_send
@property
def max_bson_size(self) -> int:
"""A proxy for SockInfo.max_bson_size."""
@ -627,22 +633,6 @@ class _BulkWriteContext:
"""The maximum size of a BSON command before batch splitting."""
return self.max_bson_size
def _start(
self, cmd: MutableMapping[str, Any], request_id: int, docs: list[Mapping[str, Any]]
) -> MutableMapping[str, Any]:
"""Publish a CommandStartedEvent."""
cmd[self.field] = docs
self.listeners.publish_command_start(
cmd,
self.db_name,
request_id,
self.conn.address,
self.conn.server_connection_id,
self.op_id,
self.conn.service_id,
)
return cmd
def _succeed(self, request_id: int, reply: _DocumentOut, duration: datetime.timedelta) -> None:
"""Publish a CommandSucceededEvent."""
self.listeners.publish_command_success(
@ -672,6 +662,61 @@ class _BulkWriteContext:
)
class _BulkWriteContext(_BulkWriteContextBase):
"""A wrapper around AsyncConnection/Connection for use with the collection-level bulk write API."""
__slots__ = ()
def __init__(
self,
database_name: str,
cmd_name: str,
conn: _AgnosticConnection,
operation_id: int,
listeners: _EventListeners,
session: Optional[_AgnosticClientSession],
op_type: int,
codec: CodecOptions,
):
super().__init__(
database_name,
cmd_name,
conn,
operation_id,
listeners,
session,
op_type,
codec,
)
def batch_command(
self, cmd: MutableMapping[str, Any], docs: list[Mapping[str, Any]]
) -> tuple[int, Union[bytes, dict[str, Any]], list[Mapping[str, Any]]]:
namespace = self.db_name + ".$cmd"
request_id, msg, to_send = _do_batched_op_msg(
namespace, self.op_type, cmd, docs, self.codec, self
)
if not to_send:
raise InvalidOperation("cannot do an empty bulk write")
return request_id, msg, to_send
def _start(
self, cmd: MutableMapping[str, Any], request_id: int, docs: list[Mapping[str, Any]]
) -> MutableMapping[str, Any]:
"""Publish a CommandStartedEvent."""
cmd[self.field] = docs
self.listeners.publish_command_start(
cmd,
self.db_name,
request_id,
self.conn.address,
self.conn.server_connection_id,
self.op_id,
self.conn.service_id,
)
return cmd
class _EncryptedBulkWriteContext(_BulkWriteContext):
__slots__ = ()
@ -878,6 +923,304 @@ def _do_batched_op_msg(
return _batched_op_msg(operation, command, docs, ack, opts, ctx)
class _ClientBulkWriteContext(_BulkWriteContextBase):
"""A wrapper around AsyncConnection/Connection for use with the client-level bulk write API."""
__slots__ = ()
def __init__(
self,
database_name: str,
cmd_name: str,
conn: _AgnosticConnection,
operation_id: int,
listeners: _EventListeners,
session: Optional[_AgnosticClientSession],
codec: CodecOptions,
):
super().__init__(
database_name,
cmd_name,
conn,
operation_id,
listeners,
session,
0,
codec,
)
def batch_command(
self, cmd: MutableMapping[str, Any], operations: list[tuple[str, Mapping[str, Any]]]
) -> tuple[int, Union[bytes, dict[str, Any]], list[Mapping[str, Any]], list[Mapping[str, Any]]]:
request_id, msg, to_send_ops, to_send_ns = _client_do_batched_op_msg(
cmd, operations, self.codec, self
)
if not to_send_ops:
raise InvalidOperation("cannot do an empty bulk write")
return request_id, msg, to_send_ops, to_send_ns
def _start(
self,
cmd: MutableMapping[str, Any],
request_id: int,
op_docs: list[Mapping[str, Any]],
ns_docs: list[Mapping[str, Any]],
) -> MutableMapping[str, Any]:
"""Publish a CommandStartedEvent."""
cmd["ops"] = op_docs
cmd["nsInfo"] = ns_docs
self.listeners.publish_command_start(
cmd,
self.db_name,
request_id,
self.conn.address,
self.conn.server_connection_id,
self.op_id,
self.conn.service_id,
)
return cmd
_OP_MSG_OVERHEAD = 1000
def _client_construct_op_msg(
command: Mapping[str, Any],
to_send_ops: list[Mapping[str, Any]],
to_send_ns: list[Mapping[str, Any]],
ack: bool,
opts: CodecOptions,
buf: _BytesIO,
) -> int:
# Write flags
flags = b"\x00\x00\x00\x00" if ack else b"\x02\x00\x00\x00"
buf.write(flags)
# Type 0 Section
buf.write(b"\x00")
buf.write(_dict_to_bson(command, False, opts))
# Type 1 Section for ops
buf.write(b"\x01")
size_location = buf.tell()
# Save space for size
buf.write(b"\x00\x00\x00\x00")
buf.write(b"ops\x00")
# Write all the ops documents
for op in to_send_ops:
buf.write(_dict_to_bson(op, False, opts))
resume_location = buf.tell()
# Write type 1 section size
length = buf.tell()
buf.seek(size_location)
buf.write(_pack_int(length - size_location))
buf.seek(resume_location)
# Type 1 Section for nsInfo
buf.write(b"\x01")
size_location = buf.tell()
# Save space for size
buf.write(b"\x00\x00\x00\x00")
buf.write(b"nsInfo\x00")
# Write all the nsInfo documents
for ns in to_send_ns:
buf.write(_dict_to_bson(ns, False, opts))
# Write type 1 section size
length = buf.tell()
buf.seek(size_location)
buf.write(_pack_int(length - size_location))
return length
def _client_batched_op_msg_impl(
command: Mapping[str, Any],
operations: list[tuple[str, Mapping[str, Any]]],
ack: bool,
opts: CodecOptions,
ctx: _ClientBulkWriteContext,
buf: _BytesIO,
) -> tuple[list[Mapping[str, Any]], list[Mapping[str, Any]], int]:
"""Create a batched OP_MSG write for client-level bulk write."""
def _check_doc_size_limits(
op_type: str,
document: Mapping[str, Any],
limit: int,
) -> int:
doc_size = len(_dict_to_bson(document, False, opts))
if doc_size > limit:
_raise_document_too_large(op_type, doc_size, limit)
return doc_size
max_bson_size = ctx.max_bson_size
max_write_batch_size = ctx.max_write_batch_size
max_message_size = ctx.max_message_size
# Don't include bulkWrite-command-agnostic fields in document size calculations.
abridged_keys = ["bulkWrite", "errorsOnly", "ordered"]
if command.get("bypassDocumentValidation"):
abridged_keys.append("bypassDocumentValidation")
if command.get("comment"):
abridged_keys.append("comment")
if command.get("let"):
abridged_keys.append("let")
command_abridged = {key: command[key] for key in abridged_keys}
command_len_abridged = len(_dict_to_bson(command_abridged, False, opts))
# When OP_MSG is used unacknowledged we have to check command
# document size client-side or applications won't be notified.
if not ack:
_check_doc_size_limits("bulkWrite", command_abridged, max_bson_size + _COMMAND_OVERHEAD)
# Maximum combined size of the ops and nsInfo document sequences.
max_doc_sequences_bytes = max_message_size - (_OP_MSG_OVERHEAD + command_len_abridged)
ns_info = {}
to_send_ops: list[Mapping[str, Any]] = []
to_send_ns: list[Mapping[str, int]] = []
total_ops_length = 0
total_ns_length = 0
idx = 0
for real_op_type, op_doc in operations:
op_type = real_op_type
# Check insert/replace document size if unacknowledged.
if real_op_type == "insert":
if not ack:
_check_doc_size_limits(real_op_type, op_doc["document"], max_bson_size)
if real_op_type == "replace":
op_type = "update"
if not ack:
_check_doc_size_limits(real_op_type, op_doc["updateMods"], max_bson_size)
ns_doc_to_send = None
ns_length = 0
namespace = op_doc[op_type]
if namespace not in ns_info:
ns_doc_to_send = {"ns": namespace}
new_ns_index = len(to_send_ns)
ns_info[namespace] = new_ns_index
# First entry in the operation doc has the operation type as its
# key and the index of its namespace within ns_info as its value.
op_doc_to_send = copy.deepcopy(op_doc)
op_doc_to_send[op_type] = ns_info[namespace] # type: ignore[index]
# Encode current operation doc and, if newly added, namespace doc.
op_length = len(_dict_to_bson(op_doc_to_send, False, opts))
if ns_doc_to_send:
ns_length = len(_dict_to_bson(ns_doc_to_send, False, opts))
# Check operation document size if unacknowledged.
if not ack:
_check_doc_size_limits(op_type, op_doc_to_send, max_bson_size + _COMMAND_OVERHEAD)
new_message_size = total_ops_length + total_ns_length + op_length + ns_length
# We have enough data, return this batch.
if new_message_size > max_doc_sequences_bytes:
break
to_send_ops.append(op_doc_to_send)
total_ops_length += op_length
if ns_doc_to_send:
to_send_ns.append(ns_doc_to_send)
total_ns_length += ns_length
idx += 1
# We have enough documents, return this batch.
if idx == max_write_batch_size:
break
# Construct the entire OP_MSG.
length = _client_construct_op_msg(command, to_send_ops, to_send_ns, ack, opts, buf)
return to_send_ops, to_send_ns, length
def _client_encode_batched_op_msg(
command: Mapping[str, Any],
operations: list[tuple[str, Mapping[str, Any]]],
ack: bool,
opts: CodecOptions,
ctx: _ClientBulkWriteContext,
) -> tuple[bytes, list[Mapping[str, Any]], list[Mapping[str, Any]]]:
"""Encode the next batched client-level bulkWrite
operation as OP_MSG.
"""
buf = _BytesIO()
to_send_ops, to_send_ns, _ = _client_batched_op_msg_impl(
command, operations, ack, opts, ctx, buf
)
return buf.getvalue(), to_send_ops, to_send_ns
def _client_batched_op_msg_compressed(
command: Mapping[str, Any],
operations: list[tuple[str, Mapping[str, Any]]],
ack: bool,
opts: CodecOptions,
ctx: _ClientBulkWriteContext,
) -> tuple[int, bytes, list[Mapping[str, Any]], list[Mapping[str, Any]]]:
"""Create the next batched client-level bulkWrite operation
with OP_MSG, compressed.
"""
data, to_send_ops, to_send_ns = _client_encode_batched_op_msg(
command, operations, ack, opts, ctx
)
assert ctx.conn.compression_context is not None
request_id, msg = _compress(2013, data, ctx.conn.compression_context)
return request_id, msg, to_send_ops, to_send_ns
def _client_batched_op_msg(
command: Mapping[str, Any],
operations: list[tuple[str, Mapping[str, Any]]],
ack: bool,
opts: CodecOptions,
ctx: _ClientBulkWriteContext,
) -> tuple[int, bytes, list[Mapping[str, Any]], list[Mapping[str, Any]]]:
"""OP_MSG implementation entry point for client-level bulkWrite."""
buf = _BytesIO()
# Save space for message length and request id
buf.write(_ZERO_64)
# responseTo, opCode
buf.write(b"\x00\x00\x00\x00\xdd\x07\x00\x00")
to_send_ops, to_send_ns, length = _client_batched_op_msg_impl(
command, operations, ack, opts, ctx, buf
)
# Header - request id and message length
buf.seek(4)
request_id = _randint()
buf.write(_pack_int(request_id))
buf.seek(0)
buf.write(_pack_int(length))
return request_id, buf.getvalue(), to_send_ops, to_send_ns
def _client_do_batched_op_msg(
command: MutableMapping[str, Any],
operations: list[tuple[str, Mapping[str, Any]]],
opts: CodecOptions,
ctx: _ClientBulkWriteContext,
) -> tuple[int, bytes, list[Mapping[str, Any]], list[Mapping[str, Any]]]:
"""Create the next batched client-level bulkWrite
operation using OP_MSG.
"""
command["$db"] = "admin"
if "writeConcern" in command:
ack = bool(command["writeConcern"].get("w", 1))
else:
ack = True
if ctx.conn.compression_context:
return _client_batched_op_msg_compressed(command, operations, ack, opts, ctx)
return _client_batched_op_msg(command, operations, ack, opts, ctx)
# End OP_MSG -----------------------------------------------------

View File

@ -19,7 +19,7 @@ from __future__ import annotations
from collections import namedtuple
from datetime import datetime as _datetime
from datetime import timezone
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Optional
from pymongo.lock import _create_lock
@ -27,6 +27,22 @@ if TYPE_CHECKING:
from cryptography.x509.ocsp import OCSPRequest, OCSPResponse
def _next_update(value: OCSPResponse) -> Optional[_datetime]:
"""Compat helper to return the response's next_update_utc."""
# Added in cryptography 43.0.0.
if hasattr(value, "next_update_utc"):
return value.next_update_utc
return value.next_update
def _this_update(value: OCSPResponse) -> Optional[_datetime]:
"""Compat helper to return the response's this_update_utc."""
# Added in cryptography 43.0.0.
if hasattr(value, "this_update_utc"):
return value.this_update_utc
return value.this_update
class _OCSPCache:
"""A cache for OCSP responses."""
@ -62,25 +78,30 @@ class _OCSPCache:
# As per the OCSP protocol, if the response's nextUpdate field is
# not set, the responder is indicating that newer revocation
# information is available all the time.
if value.next_update is None:
next_update = _next_update(value)
if next_update is None:
self._data.pop(cache_key, None)
return
this_update = _this_update(value)
if this_update is None:
return
now = _datetime.now(tz=timezone.utc)
if this_update.tzinfo is None:
# Make naive to match cryptography.
now = now.replace(tzinfo=None)
# Do nothing if the response is invalid.
if not (
value.this_update
<= _datetime.now(tz=timezone.utc).replace(tzinfo=None)
< value.next_update
):
if not (this_update <= now < next_update):
return
# Cache new response OR update cached response if new response
# has longer validity.
cached_value = self._data.get(cache_key, None)
if cached_value is None or (
cached_value.next_update is not None
and cached_value.next_update < value.next_update
):
if cached_value is None:
self._data[cache_key] = value
return
cached_next_update = _next_update(cached_value)
if cached_next_update is not None and cached_next_update < next_update:
self._data[cache_key] = value
def __getitem__(self, item: OCSPRequest) -> OCSPResponse:
@ -95,13 +116,15 @@ class _OCSPCache:
value = self._data[cache_key]
# Return cached response if it is still valid.
assert value.this_update is not None
assert value.next_update is not None
if (
value.this_update
<= _datetime.now(tz=timezone.utc).replace(tzinfo=None)
< value.next_update
):
this_update = _this_update(value)
next_update = _next_update(value)
assert this_update is not None
assert next_update is not None
now = _datetime.now(tz=timezone.utc)
if this_update.tzinfo is None:
# Make naive to match cryptography.
now = now.replace(tzinfo=None)
if this_update <= now < next_update:
return value
self._data.pop(cache_key, None)

View File

@ -58,6 +58,7 @@ from requests import post as _post
from requests.exceptions import RequestException as _RequestException
from pymongo import _csot
from pymongo.ocsp_cache import _next_update, _this_update
if TYPE_CHECKING:
from cryptography.hazmat.primitives.asymmetric import (
@ -275,13 +276,18 @@ def _verify_response(issuer: Certificate, response: OCSPResponse) -> int:
# Note that we are not using a "tolerance period" as discussed in
# https://tools.ietf.org/rfc/rfc5019.txt?
now = _datetime.now(tz=timezone.utc).replace(tzinfo=None)
this_update = _this_update(response)
now = _datetime.now(tz=timezone.utc)
if this_update and this_update.tzinfo is None:
# Make naive to match cryptography.
now = now.replace(tzinfo=None)
# RFC6960, Section 3.2, Number 5
if response.this_update > now:
if this_update and this_update > now:
_LOGGER.debug("thisUpdate is in the future")
return 0
# RFC6960, Section 3.2, Number 6
if response.next_update and response.next_update < now:
next_update = _next_update(response)
if next_update and next_update < now:
_LOGGER.debug("nextUpdate is in the past")
return 0
return 1

View File

@ -34,12 +34,13 @@ from bson.raw_bson import RawBSONDocument
from pymongo import helpers_shared
from pymongo.collation import validate_collation_or_none
from pymongo.common import validate_is_mapping, validate_list
from pymongo.errors import InvalidOperation
from pymongo.helpers_shared import _gen_index_name, _index_document, _index_list
from pymongo.typings import _CollationIn, _DocumentType, _Pipeline
from pymongo.write_concern import validate_boolean
if TYPE_CHECKING:
from pymongo.typings import _AgnosticBulk
from pymongo.typings import _AgnosticBulk, _AgnosticClientBulk
# Hint supports index name, "myIndex", a list of either strings or index pairs: [('x', 1), ('y', -1), 'z''], or a dictionary
@ -52,6 +53,7 @@ _IndexKeyHint = Union[str, _IndexList]
class _Op(str, enum.Enum):
ABORT = "abortTransaction"
AGGREGATE = "aggregate"
BULK_WRITE = "bulkWrite"
COMMIT = "commitTransaction"
COUNT = "count"
CREATE = "create"
@ -83,48 +85,130 @@ class _Op(str, enum.Enum):
class InsertOne(Generic[_DocumentType]):
"""Represents an insert_one operation."""
__slots__ = ("_doc",)
__slots__ = (
"_doc",
"_namespace",
)
def __init__(self, document: _DocumentType) -> None:
def __init__(self, document: _DocumentType, namespace: Optional[str] = None) -> None:
"""Create an InsertOne instance.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write` and :meth:`~pymongo.collection.Collection.bulk_write`.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write`, :meth:`~pymongo.collection.Collection.bulk_write`,
:meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.bulk_write` and :meth:`~pymongo.mongo_client.MongoClient.bulk_write`.
:param document: The document to insert. If the document is missing an
_id field one will be added.
:param namespace: (optional) The namespace in which to insert a document.
.. versionchanged:: 4.9
Added the `namespace` option to support `MongoClient.bulk_write`.
"""
self._doc = document
self._namespace = namespace
def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None:
"""Add this operation to the _AsyncBulk/_Bulk instance `bulkobj`."""
bulkobj.add_insert(self._doc) # type: ignore[arg-type]
def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None:
"""Add this operation to the _AsyncClientBulk/_ClientBulk instance `bulkobj`."""
if not self._namespace:
raise InvalidOperation(
"MongoClient.bulk_write requires a namespace to be provided for each write operation"
)
bulkobj.add_insert(
self._namespace,
self._doc, # type: ignore[arg-type]
)
def __repr__(self) -> str:
return f"InsertOne({self._doc!r})"
if self._namespace:
return f"{self.__class__.__name__}({self._doc!r}, {self._namespace!r})"
return f"{self.__class__.__name__}({self._doc!r})"
def __eq__(self, other: Any) -> bool:
if type(other) == type(self):
return other._doc == self._doc
return other._doc == self._doc and other._namespace == self._namespace
return NotImplemented
def __ne__(self, other: Any) -> bool:
return not self == other
class DeleteOne:
"""Represents a delete_one operation."""
class _DeleteOp:
"""Private base class for delete operations."""
__slots__ = ("_filter", "_collation", "_hint")
__slots__ = (
"_filter",
"_collation",
"_hint",
"_namespace",
)
def __init__(
self,
filter: Mapping[str, Any],
collation: Optional[_CollationIn] = None,
hint: Optional[_IndexKeyHint] = None,
namespace: Optional[str] = None,
) -> None:
if filter is not None:
validate_is_mapping("filter", filter)
if hint is not None and not isinstance(hint, str):
self._hint: Union[str, dict[str, Any], None] = helpers_shared._index_document(hint)
else:
self._hint = hint
self._filter = filter
self._collation = collation
self._namespace = namespace
def __eq__(self, other: Any) -> bool:
if type(other) == type(self):
return (
other._filter,
other._collation,
other._hint,
other._namespace,
) == (
self._filter,
self._collation,
self._hint,
self._namespace,
)
return NotImplemented
def __ne__(self, other: Any) -> bool:
return not self == other
def __repr__(self) -> str:
if self._namespace:
return "{}({!r}, {!r}, {!r}, {!r})".format(
self.__class__.__name__,
self._filter,
self._collation,
self._hint,
self._namespace,
)
return f"{self.__class__.__name__}({self._filter!r}, {self._collation!r}, {self._hint!r})"
class DeleteOne(_DeleteOp):
"""Represents a delete_one operation."""
__slots__ = ()
def __init__(
self,
filter: Mapping[str, Any],
collation: Optional[_CollationIn] = None,
hint: Optional[_IndexKeyHint] = None,
namespace: Optional[str] = None,
) -> None:
"""Create a DeleteOne instance.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write` and :meth:`~pymongo.collection.Collection.bulk_write`.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write`, :meth:`~pymongo.collection.Collection.bulk_write`,
:meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.bulk_write` and :meth:`~pymongo.mongo_client.MongoClient.bulk_write`.
:param filter: A query that matches the document to delete.
:param collation: An instance of
@ -135,20 +219,16 @@ class DeleteOne:
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` or :meth:`~pymongo.collection.Collection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.4 and above.
:param namespace: (optional) The namespace in which to delete a document.
.. versionchanged:: 4.9
Added the `namespace` option to support `MongoClient.bulk_write`.
.. versionchanged:: 3.11
Added the ``hint`` option.
.. versionchanged:: 3.5
Added the `collation` option.
"""
if filter is not None:
validate_is_mapping("filter", filter)
if hint is not None and not isinstance(hint, str):
self._hint: Union[str, dict[str, Any], None] = helpers_shared._index_document(hint)
else:
self._hint = hint
self._filter = filter
self._collation = collation
super().__init__(filter, collation, hint, namespace)
def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None:
"""Add this operation to the _AsyncBulk/_Bulk instance `bulkobj`."""
@ -159,36 +239,37 @@ class DeleteOne:
hint=self._hint,
)
def __repr__(self) -> str:
return f"DeleteOne({self._filter!r}, {self._collation!r}, {self._hint!r})"
def __eq__(self, other: Any) -> bool:
if type(other) == type(self):
return (other._filter, other._collation, other._hint) == (
self._filter,
self._collation,
self._hint,
def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None:
"""Add this operation to the _AsyncClientBulk/_ClientBulk instance `bulkobj`."""
if not self._namespace:
raise InvalidOperation(
"MongoClient.bulk_write requires a namespace to be provided for each write operation"
)
return NotImplemented
def __ne__(self, other: Any) -> bool:
return not self == other
bulkobj.add_delete(
self._namespace,
self._filter,
multi=False,
collation=validate_collation_or_none(self._collation),
hint=self._hint,
)
class DeleteMany:
class DeleteMany(_DeleteOp):
"""Represents a delete_many operation."""
__slots__ = ("_filter", "_collation", "_hint")
__slots__ = ()
def __init__(
self,
filter: Mapping[str, Any],
collation: Optional[_CollationIn] = None,
hint: Optional[_IndexKeyHint] = None,
namespace: Optional[str] = None,
) -> None:
"""Create a DeleteMany instance.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write` and :meth:`~pymongo.collection.Collection.bulk_write`.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write`, :meth:`~pymongo.collection.Collection.bulk_write`,
:meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.bulk_write` and :meth:`~pymongo.mongo_client.MongoClient.bulk_write`.
:param filter: A query that matches the documents to delete.
:param collation: An instance of
@ -199,20 +280,16 @@ class DeleteMany:
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` or :meth:`~pymongo.collection.Collection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.4 and above.
:param namespace: (optional) The namespace in which to delete documents.
.. versionchanged:: 4.9
Added the `namespace` option to support `MongoClient.bulk_write`.
.. versionchanged:: 3.11
Added the ``hint`` option.
.. versionchanged:: 3.5
Added the `collation` option.
"""
if filter is not None:
validate_is_mapping("filter", filter)
if hint is not None and not isinstance(hint, str):
self._hint: Union[str, dict[str, Any], None] = helpers_shared._index_document(hint)
else:
self._hint = hint
self._filter = filter
self._collation = collation
super().__init__(filter, collation, hint, namespace)
def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None:
"""Add this operation to the _AsyncBulk/_Bulk instance `bulkobj`."""
@ -223,26 +300,32 @@ class DeleteMany:
hint=self._hint,
)
def __repr__(self) -> str:
return f"DeleteMany({self._filter!r}, {self._collation!r}, {self._hint!r})"
def __eq__(self, other: Any) -> bool:
if type(other) == type(self):
return (other._filter, other._collation, other._hint) == (
self._filter,
self._collation,
self._hint,
def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None:
"""Add this operation to the _AsyncClientBulk/_ClientBulk instance `bulkobj`."""
if not self._namespace:
raise InvalidOperation(
"MongoClient.bulk_write requires a namespace to be provided for each write operation"
)
return NotImplemented
def __ne__(self, other: Any) -> bool:
return not self == other
bulkobj.add_delete(
self._namespace,
self._filter,
multi=True,
collation=validate_collation_or_none(self._collation),
hint=self._hint,
)
class ReplaceOne(Generic[_DocumentType]):
"""Represents a replace_one operation."""
__slots__ = ("_filter", "_doc", "_upsert", "_collation", "_hint")
__slots__ = (
"_filter",
"_doc",
"_upsert",
"_collation",
"_hint",
"_namespace",
)
def __init__(
self,
@ -251,10 +334,12 @@ class ReplaceOne(Generic[_DocumentType]):
upsert: bool = False,
collation: Optional[_CollationIn] = None,
hint: Optional[_IndexKeyHint] = None,
namespace: Optional[str] = None,
) -> None:
"""Create a ReplaceOne instance.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write` and :meth:`~pymongo.collection.Collection.bulk_write`.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write`, :meth:`~pymongo.collection.Collection.bulk_write`,
:meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.bulk_write` and :meth:`~pymongo.mongo_client.MongoClient.bulk_write`.
:param filter: A query that matches the document to replace.
:param replacement: The new document.
@ -268,7 +353,10 @@ class ReplaceOne(Generic[_DocumentType]):
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` or :meth:`~pymongo.collection.Collection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.2 and above.
:param namespace: (optional) The namespace in which to replace a document.
.. versionchanged:: 4.9
Added the `namespace` option to support `MongoClient.bulk_write`.
.. versionchanged:: 3.11
Added the ``hint`` option.
.. versionchanged:: 3.5
@ -282,10 +370,12 @@ class ReplaceOne(Generic[_DocumentType]):
self._hint: Union[str, dict[str, Any], None] = helpers_shared._index_document(hint)
else:
self._hint = hint
self._filter = filter
self._doc = replacement
self._upsert = upsert
self._collation = collation
self._namespace = namespace
def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None:
"""Add this operation to the _AsyncBulk/_Bulk instance `bulkobj`."""
@ -297,6 +387,21 @@ class ReplaceOne(Generic[_DocumentType]):
hint=self._hint,
)
def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None:
"""Add this operation to the _AsyncClientBulk/_ClientBulk instance `bulkobj`."""
if not self._namespace:
raise InvalidOperation(
"MongoClient.bulk_write requires a namespace to be provided for each write operation"
)
bulkobj.add_replace(
self._namespace,
self._filter,
self._doc,
self._upsert,
collation=validate_collation_or_none(self._collation),
hint=self._hint,
)
def __eq__(self, other: Any) -> bool:
if type(other) == type(self):
return (
@ -305,12 +410,14 @@ class ReplaceOne(Generic[_DocumentType]):
other._upsert,
other._collation,
other._hint,
other._namespace,
) == (
self._filter,
self._doc,
self._upsert,
self._collation,
other._hint,
self._namespace,
)
return NotImplemented
@ -318,6 +425,16 @@ class ReplaceOne(Generic[_DocumentType]):
return not self == other
def __repr__(self) -> str:
if self._namespace:
return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format(
self.__class__.__name__,
self._filter,
self._doc,
self._upsert,
self._collation,
self._hint,
self._namespace,
)
return "{}({!r}, {!r}, {!r}, {!r}, {!r})".format(
self.__class__.__name__,
self._filter,
@ -331,16 +448,25 @@ class ReplaceOne(Generic[_DocumentType]):
class _UpdateOp:
"""Private base class for update operations."""
__slots__ = ("_filter", "_doc", "_upsert", "_collation", "_array_filters", "_hint")
__slots__ = (
"_filter",
"_doc",
"_upsert",
"_collation",
"_array_filters",
"_hint",
"_namespace",
)
def __init__(
self,
filter: Mapping[str, Any],
doc: Union[Mapping[str, Any], _Pipeline],
upsert: bool,
upsert: Optional[bool],
collation: Optional[_CollationIn],
array_filters: Optional[list[Mapping[str, Any]]],
hint: Optional[_IndexKeyHint],
namespace: Optional[str],
):
if filter is not None:
validate_is_mapping("filter", filter)
@ -358,6 +484,7 @@ class _UpdateOp:
self._upsert = upsert
self._collation = collation
self._array_filters = array_filters
self._namespace = namespace
def __eq__(self, other: object) -> bool:
if isinstance(other, type(self)):
@ -368,6 +495,7 @@ class _UpdateOp:
other._collation,
other._array_filters,
other._hint,
other._namespace,
) == (
self._filter,
self._doc,
@ -375,10 +503,25 @@ class _UpdateOp:
self._collation,
self._array_filters,
self._hint,
self._namespace,
)
return NotImplemented
def __ne__(self, other: Any) -> bool:
return not self == other
def __repr__(self) -> str:
if self._namespace:
return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format(
self.__class__.__name__,
self._filter,
self._doc,
self._upsert,
self._collation,
self._array_filters,
self._hint,
self._namespace,
)
return "{}({!r}, {!r}, {!r}, {!r}, {!r}, {!r})".format(
self.__class__.__name__,
self._filter,
@ -399,14 +542,16 @@ class UpdateOne(_UpdateOp):
self,
filter: Mapping[str, Any],
update: Union[Mapping[str, Any], _Pipeline],
upsert: bool = False,
upsert: Optional[bool] = None,
collation: Optional[_CollationIn] = None,
array_filters: Optional[list[Mapping[str, Any]]] = None,
hint: Optional[_IndexKeyHint] = None,
namespace: Optional[str] = None,
) -> None:
"""Represents an update_one operation.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write` and :meth:`~pymongo.collection.Collection.bulk_write`.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write`, :meth:`~pymongo.collection.Collection.bulk_write`,
:meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.bulk_write` and :meth:`~pymongo.mongo_client.MongoClient.bulk_write`.
:param filter: A query that matches the document to update.
:param update: The modifications to apply.
@ -422,7 +567,10 @@ class UpdateOne(_UpdateOp):
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` or :meth:`~pymongo.collection.Collection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.2 and above.
:param namespace: (optional) The namespace in which to update a document.
.. versionchanged:: 4.9
Added the `namespace` option to support `MongoClient.bulk_write`.
.. versionchanged:: 3.11
Added the `hint` option.
.. versionchanged:: 3.9
@ -432,11 +580,28 @@ class UpdateOne(_UpdateOp):
.. versionchanged:: 3.5
Added the `collation` option.
"""
super().__init__(filter, update, upsert, collation, array_filters, hint)
super().__init__(filter, update, upsert, collation, array_filters, hint, namespace)
def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None:
"""Add this operation to the _AsyncBulk/_Bulk instance `bulkobj`."""
bulkobj.add_update(
self._filter,
self._doc,
False,
bool(self._upsert),
collation=validate_collation_or_none(self._collation),
array_filters=self._array_filters,
hint=self._hint,
)
def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None:
"""Add this operation to the _AsyncClientBulk/_ClientBulk instance `bulkobj`."""
if not self._namespace:
raise InvalidOperation(
"MongoClient.bulk_write requires a namespace to be provided for each write operation"
)
bulkobj.add_update(
self._namespace,
self._filter,
self._doc,
False,
@ -456,14 +621,16 @@ class UpdateMany(_UpdateOp):
self,
filter: Mapping[str, Any],
update: Union[Mapping[str, Any], _Pipeline],
upsert: bool = False,
upsert: Optional[bool] = None,
collation: Optional[_CollationIn] = None,
array_filters: Optional[list[Mapping[str, Any]]] = None,
hint: Optional[_IndexKeyHint] = None,
namespace: Optional[str] = None,
) -> None:
"""Create an UpdateMany instance.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write` and :meth:`~pymongo.collection.Collection.bulk_write`.
For use with :meth:`~pymongo.asynchronous.collection.AsyncCollection.bulk_write`, :meth:`~pymongo.collection.Collection.bulk_write`,
:meth:`~pymongo.asynchronous.mongo_client.AsyncMongoClient.bulk_write` and :meth:`~pymongo.mongo_client.MongoClient.bulk_write`.
:param filter: A query that matches the documents to update.
:param update: The modifications to apply.
@ -479,7 +646,10 @@ class UpdateMany(_UpdateOp):
:meth:`~pymongo.asynchronous.collection.AsyncCollection.create_index` or :meth:`~pymongo.collection.Collection.create_index` (e.g.
``[('field', ASCENDING)]``). This option is only supported on
MongoDB 4.2 and above.
:param namespace: (optional) The namespace in which to update documents.
.. versionchanged:: 4.9
Added the `namespace` option to support `MongoClient.bulk_write`.
.. versionchanged:: 3.11
Added the `hint` option.
.. versionchanged:: 3.9
@ -489,11 +659,28 @@ class UpdateMany(_UpdateOp):
.. versionchanged:: 3.5
Added the `collation` option.
"""
super().__init__(filter, update, upsert, collation, array_filters, hint)
super().__init__(filter, update, upsert, collation, array_filters, hint, namespace)
def _add_to_bulk(self, bulkobj: _AgnosticBulk) -> None:
"""Add this operation to the _AsyncBulk/_Bulk instance `bulkobj`."""
bulkobj.add_update(
self._filter,
self._doc,
True,
bool(self._upsert),
collation=validate_collation_or_none(self._collation),
array_filters=self._array_filters,
hint=self._hint,
)
def _add_to_client_bulk(self, bulkobj: _AgnosticClientBulk) -> None:
"""Add this operation to the _AsyncClientBulk/_ClientBulk instance `bulkobj`."""
if not self._namespace:
raise InvalidOperation(
"MongoClient.bulk_write requires a namespace to be provided for each write operation"
)
bulkobj.add_update(
self._namespace,
self._filter,
self._doc,
True,

View File

@ -607,10 +607,7 @@ class MovingAverage:
def add_sample(self, sample: float) -> None:
if sample < 0:
# Likely system time change while waiting for hello response
# and not using time.monotonic. Ignore it, the next one will
# probably be valid.
return
raise ValueError(f"duration cannot be negative {sample}")
if self.average is None:
self.average = sample
else:

View File

@ -18,7 +18,7 @@
"""
from __future__ import annotations
from typing import Any, Mapping, Optional, cast
from typing import Any, Mapping, MutableMapping, Optional, cast
from pymongo.errors import InvalidOperation
@ -65,7 +65,9 @@ class _WriteResult:
class InsertOneResult(_WriteResult):
"""The return type for :meth:`~pymongo.collection.Collection.insert_one`."""
"""The return type for :meth:`~pymongo.collection.Collection.insert_one`
and as part of :meth:`~pymongo.mongo_client.MongoClient.bulk_write`.
"""
__slots__ = ("__inserted_id",)
@ -113,13 +115,23 @@ class InsertManyResult(_WriteResult):
class UpdateResult(_WriteResult):
"""The return type for :meth:`~pymongo.collection.Collection.update_one`,
:meth:`~pymongo.collection.Collection.update_many`, and
:meth:`~pymongo.collection.Collection.replace_one`.
:meth:`~pymongo.collection.Collection.replace_one`, and as part of
:meth:`~pymongo.mongo_client.MongoClient.bulk_write`.
"""
__slots__ = ("__raw_result",)
__slots__ = (
"__raw_result",
"__in_client_bulk",
)
def __init__(self, raw_result: Optional[Mapping[str, Any]], acknowledged: bool):
def __init__(
self,
raw_result: Optional[Mapping[str, Any]],
acknowledged: bool,
in_client_bulk: bool = False,
):
self.__raw_result = raw_result
self.__in_client_bulk = in_client_bulk
super().__init__(acknowledged)
def __repr__(self) -> str:
@ -134,9 +146,9 @@ class UpdateResult(_WriteResult):
def matched_count(self) -> int:
"""The number of documents matched for this update."""
self._raise_if_unacknowledged("matched_count")
if self.upserted_id is not None:
return 0
assert self.__raw_result is not None
if not self.__in_client_bulk and self.upserted_id is not None:
return 0
return self.__raw_result.get("n", 0)
@property
@ -153,12 +165,21 @@ class UpdateResult(_WriteResult):
"""
self._raise_if_unacknowledged("upserted_id")
assert self.__raw_result is not None
return self.__raw_result.get("upserted")
if self.__in_client_bulk and self.__raw_result.get("upserted"):
return self.__raw_result["upserted"]["_id"]
return self.__raw_result.get("upserted", None)
@property
def did_upsert(self) -> bool:
"""Whether or not an upsert took place."""
assert self.__raw_result is not None
return len(self.__raw_result.get("upserted", {})) > 0
class DeleteResult(_WriteResult):
"""The return type for :meth:`~pymongo.collection.Collection.delete_one`
and :meth:`~pymongo.collection.Collection.delete_many`
and as part of :meth:`~pymongo.mongo_client.MongoClient.bulk_write`.
"""
__slots__ = ("__raw_result",)
@ -182,19 +203,12 @@ class DeleteResult(_WriteResult):
return self.__raw_result.get("n", 0)
class BulkWriteResult(_WriteResult):
"""An object wrapper for bulk API write results."""
class _BulkWriteResultBase(_WriteResult):
"""Private base class for bulk write API results."""
__slots__ = ("__bulk_api_result",)
def __init__(self, bulk_api_result: dict[str, Any], acknowledged: bool) -> None:
"""Create a BulkWriteResult instance.
:param bulk_api_result: A result dict from the bulk API
:param acknowledged: Was this write result acknowledged? If ``False``
then all properties of this object will raise
:exc:`~pymongo.errors.InvalidOperation`.
"""
self.__bulk_api_result = bulk_api_result
super().__init__(acknowledged)
@ -203,7 +217,7 @@ class BulkWriteResult(_WriteResult):
@property
def bulk_api_result(self) -> dict[str, Any]:
"""The raw bulk API result."""
"""The raw bulk write API result."""
return self.__bulk_api_result
@property
@ -228,7 +242,10 @@ class BulkWriteResult(_WriteResult):
def deleted_count(self) -> int:
"""The number of documents deleted."""
self._raise_if_unacknowledged("deleted_count")
return cast(int, self.__bulk_api_result.get("nRemoved"))
if "nRemoved" in self.__bulk_api_result:
return cast(int, self.__bulk_api_result.get("nRemoved"))
else:
return cast(int, self.__bulk_api_result.get("nDeleted"))
@property
def upserted_count(self) -> int:
@ -236,10 +253,112 @@ class BulkWriteResult(_WriteResult):
self._raise_if_unacknowledged("upserted_count")
return cast(int, self.__bulk_api_result.get("nUpserted"))
class BulkWriteResult(_BulkWriteResultBase):
"""An object wrapper for collection-level bulk write API results."""
__slots__ = ()
def __init__(self, bulk_api_result: dict[str, Any], acknowledged: bool) -> None:
"""Create a BulkWriteResult instance.
:param bulk_api_result: A result dict from the collection-level bulk write API
:param acknowledged: Was this write result acknowledged? If ``False``
then all properties of this object will raise
:exc:`~pymongo.errors.InvalidOperation`.
"""
super().__init__(bulk_api_result, acknowledged)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}({self.bulk_api_result!r}, acknowledged={self.acknowledged})"
)
@property
def upserted_ids(self) -> Optional[dict[int, Any]]:
"""A map of operation index to the _id of the upserted document."""
self._raise_if_unacknowledged("upserted_ids")
if self.__bulk_api_result:
if self.bulk_api_result:
return {upsert["index"]: upsert["_id"] for upsert in self.bulk_api_result["upserted"]}
return None
class ClientBulkWriteResult(_BulkWriteResultBase):
"""An object wrapper for client-level bulk write API results."""
__slots__ = ("__has_verbose_results",)
def __init__(
self,
bulk_api_result: MutableMapping[str, Any],
acknowledged: bool,
has_verbose_results: bool,
) -> None:
"""Create a ClientBulkWriteResult instance.
:param bulk_api_result: A result dict from the client-level bulk write API
:param acknowledged: Was this write result acknowledged? If ``False``
then all properties of this object will raise
:exc:`~pymongo.errors.InvalidOperation`.
:param has_verbose_results: Should the returned result be verbose?
If ``False``, then the ``insert_results``, ``update_results``, and
``delete_results`` properties of this object will raise
:exc:`~pymongo.errors.InvalidOperation`.
"""
self.__has_verbose_results = has_verbose_results
super().__init__(
bulk_api_result, # type: ignore[arg-type]
acknowledged,
)
def __repr__(self) -> str:
return "{}({!r}, acknowledged={}, verbose={})".format(
self.__class__.__name__,
self.bulk_api_result,
self.acknowledged,
self.has_verbose_results,
)
def _raise_if_not_verbose(self, property_name: str) -> None:
"""Raise an exception on property access if verbose results are off."""
if not self.__has_verbose_results:
raise InvalidOperation(
f"A value for {property_name} is not available when "
"the results are not set to be verbose. Check the "
"verbose_results attribute to avoid this error."
)
@property
def has_verbose_results(self) -> bool:
"""Whether the returned results should be verbose."""
return self.__has_verbose_results
@property
def insert_results(self) -> Mapping[int, InsertOneResult]:
"""A map of successful insertion operations to their results."""
self._raise_if_unacknowledged("insert_results")
self._raise_if_not_verbose("insert_results")
return cast(
Mapping[int, InsertOneResult],
self.bulk_api_result.get("insertResults"),
)
@property
def update_results(self) -> Mapping[int, UpdateResult]:
"""A map of successful update operations to their results."""
self._raise_if_unacknowledged("update_results")
self._raise_if_not_verbose("update_results")
return cast(
Mapping[int, UpdateResult],
self.bulk_api_result.get("updateResults"),
)
@property
def delete_results(self) -> Mapping[int, DeleteResult]:
"""A map of successful delete operations to their results."""
self._raise_if_unacknowledged("delete_results")
self._raise_if_not_verbose("delete_results")
return cast(
Mapping[int, DeleteResult],
self.bulk_api_result.get("deleteResults"),
)

View File

@ -297,8 +297,8 @@ class ChangeStream(Generic[_DocumentType]):
try:
resume_token = None
pipeline = [{'$match': {'operationType': 'insert'}}]
async with db.collection.watch(pipeline) as stream:
async for insert_change in stream:
with db.collection.watch(pipeline) as stream:
for insert_change in stream:
print(insert_change)
resume_token = stream.resume_token
except pymongo.errors.PyMongoError:
@ -312,9 +312,9 @@ class ChangeStream(Generic[_DocumentType]):
# Use the interrupted ChangeStream's resume token to create
# a new ChangeStream. The new stream will continue from the
# last seen insert change without missing any events.
async with db.collection.watch(
with db.collection.watch(
pipeline, resume_after=resume_token) as stream:
async for insert_change in stream:
for insert_change in stream:
print(insert_change)
Raises :exc:`StopIteration` if this ChangeStream is closed.
@ -346,9 +346,9 @@ class ChangeStream(Generic[_DocumentType]):
This method returns the next change document without waiting
indefinitely for the next change. For example::
async with db.collection.watch() as stream:
with db.collection.watch() as stream:
while stream.alive:
change = await stream.try_next()
change = stream.try_next()
# Note that the ChangeStream's resume token may be updated
# even when no changes are returned.
print("Current resume token: %r" % (stream.resume_token,))

View File

@ -0,0 +1,786 @@
# Copyright 2024-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The client-level bulk write operations interface.
.. versionadded:: 4.9
"""
from __future__ import annotations
import copy
import datetime
import logging
from collections.abc import MutableMapping
from itertools import islice
from typing import (
TYPE_CHECKING,
Any,
Mapping,
Optional,
Type,
Union,
)
from bson.objectid import ObjectId
from bson.raw_bson import RawBSONDocument
from pymongo import _csot, common
from pymongo.synchronous.client_session import ClientSession, _validate_session_write_concern
from pymongo.synchronous.collection import Collection
from pymongo.synchronous.command_cursor import CommandCursor
from pymongo.synchronous.database import Database
from pymongo.synchronous.helpers import _handle_reauth
if TYPE_CHECKING:
from pymongo.synchronous.mongo_client import MongoClient
from pymongo.synchronous.pool import Connection
from pymongo._client_bulk_shared import (
_merge_command,
_throw_client_bulk_write_exception,
)
from pymongo.common import (
validate_is_document_type,
validate_ok_for_replace,
validate_ok_for_update,
)
from pymongo.errors import (
ConfigurationError,
ConnectionFailure,
InvalidOperation,
NotPrimaryError,
OperationFailure,
WaitQueueTimeoutError,
)
from pymongo.helpers_shared import _RETRYABLE_ERROR_CODES
from pymongo.logger import _COMMAND_LOGGER, _CommandStatusMessage, _debug_log
from pymongo.message import (
_ClientBulkWriteContext,
_convert_client_bulk_exception,
_convert_exception,
_convert_write_result,
_randint,
)
from pymongo.read_preferences import ReadPreference
from pymongo.results import (
ClientBulkWriteResult,
DeleteResult,
InsertOneResult,
UpdateResult,
)
from pymongo.typings import _DocumentOut, _Pipeline
from pymongo.write_concern import WriteConcern
_IS_SYNC = True
class _ClientBulk:
"""The private guts of the client-level bulk write API."""
def __init__(
self,
client: MongoClient,
write_concern: WriteConcern,
ordered: bool = True,
bypass_document_validation: Optional[bool] = None,
comment: Optional[str] = None,
let: Optional[Any] = None,
verbose_results: bool = False,
) -> None:
"""Initialize a _ClientBulk instance."""
self.client = client
self.write_concern = write_concern
self.let = let
if self.let is not None:
common.validate_is_document_type("let", self.let)
self.ordered = ordered
self.bypass_doc_val = bypass_document_validation
self.comment = comment
self.verbose_results = verbose_results
self.ops: list[tuple[str, Mapping[str, Any]]] = []
self.idx_offset: int = 0
self.total_ops: int = 0
self.executed = False
self.uses_upsert = False
self.uses_collation = False
self.uses_array_filters = False
self.uses_hint_update = False
self.uses_hint_delete = False
self.is_retryable = self.client.options.retry_writes
self.retrying = False
self.started_retryable_write = False
@property
def bulk_ctx_class(self) -> Type[_ClientBulkWriteContext]:
return _ClientBulkWriteContext
def add_insert(self, namespace: str, document: _DocumentOut) -> None:
"""Add an insert document to the list of ops."""
validate_is_document_type("document", document)
# Generate ObjectId client side.
if not (isinstance(document, RawBSONDocument) or "_id" in document):
document["_id"] = ObjectId()
cmd = {"insert": namespace, "document": document}
self.ops.append(("insert", cmd))
self.total_ops += 1
def add_update(
self,
namespace: str,
selector: Mapping[str, Any],
update: Union[Mapping[str, Any], _Pipeline],
multi: bool = False,
upsert: Optional[bool] = None,
collation: Optional[Mapping[str, Any]] = None,
array_filters: Optional[list[Mapping[str, Any]]] = None,
hint: Union[str, dict[str, Any], None] = None,
) -> None:
"""Create an update document and add it to the list of ops."""
validate_ok_for_update(update)
cmd = {
"update": namespace,
"filter": selector,
"updateMods": update,
"multi": multi,
}
if upsert is not None:
self.uses_upsert = True
cmd["upsert"] = upsert
if array_filters is not None:
self.uses_array_filters = True
cmd["arrayFilters"] = array_filters
if hint is not None:
self.uses_hint_update = True
cmd["hint"] = hint
if collation is not None:
self.uses_collation = True
cmd["collation"] = collation
if multi:
# A bulk_write containing an update_many is not retryable.
self.is_retryable = False
self.ops.append(("update", cmd))
self.total_ops += 1
def add_replace(
self,
namespace: str,
selector: Mapping[str, Any],
replacement: Mapping[str, Any],
upsert: Optional[bool] = None,
collation: Optional[Mapping[str, Any]] = None,
hint: Union[str, dict[str, Any], None] = None,
) -> None:
"""Create a replace document and add it to the list of ops."""
validate_ok_for_replace(replacement)
cmd = {
"update": namespace,
"filter": selector,
"updateMods": replacement,
"multi": False,
}
if upsert is not None:
self.uses_upsert = True
cmd["upsert"] = upsert
if hint is not None:
self.uses_hint_update = True
cmd["hint"] = hint
if collation is not None:
self.uses_collation = True
cmd["collation"] = collation
self.ops.append(("replace", cmd))
self.total_ops += 1
def add_delete(
self,
namespace: str,
selector: Mapping[str, Any],
multi: bool,
collation: Optional[Mapping[str, Any]] = None,
hint: Union[str, dict[str, Any], None] = None,
) -> None:
"""Create a delete document and add it to the list of ops."""
cmd = {"delete": namespace, "filter": selector, "multi": multi}
if hint is not None:
self.uses_hint_delete = True
cmd["hint"] = hint
if collation is not None:
self.uses_collation = True
cmd["collation"] = collation
if multi:
# A bulk_write containing an update_many is not retryable.
self.is_retryable = False
self.ops.append(("delete", cmd))
self.total_ops += 1
@_handle_reauth
def write_command(
self,
bwc: _ClientBulkWriteContext,
cmd: MutableMapping[str, Any],
request_id: int,
msg: Union[bytes, dict[str, Any]],
op_docs: list[Mapping[str, Any]],
ns_docs: list[Mapping[str, Any]],
client: MongoClient,
) -> dict[str, Any]:
"""A proxy for Connection.write_command that handles event publishing."""
cmd["ops"] = op_docs
cmd["nsInfo"] = ns_docs
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.STARTED,
command=cmd,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
)
if bwc.publish:
bwc._start(cmd, request_id, op_docs, ns_docs)
try:
reply = bwc.conn.write_command(request_id, msg, bwc.codec) # type: ignore[misc, arg-type]
duration = datetime.datetime.now() - bwc.start_time
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.SUCCEEDED,
durationMS=duration,
reply=reply,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
)
if bwc.publish:
bwc._succeed(request_id, reply, duration) # type: ignore[arg-type]
except Exception as exc:
duration = datetime.datetime.now() - bwc.start_time
if isinstance(exc, (NotPrimaryError, OperationFailure)):
failure: _DocumentOut = exc.details # type: ignore[assignment]
else:
failure = _convert_exception(exc)
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.FAILED,
durationMS=duration,
failure=failure,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
isServerSideError=isinstance(exc, OperationFailure),
)
if bwc.publish:
bwc._fail(request_id, failure, duration)
# Top-level error will be embedded in ClientBulkWriteException.
reply = {"error": exc}
finally:
bwc.start_time = datetime.datetime.now()
return reply # type: ignore[return-value]
def unack_write(
self,
bwc: _ClientBulkWriteContext,
cmd: MutableMapping[str, Any],
request_id: int,
msg: bytes,
op_docs: list[Mapping[str, Any]],
ns_docs: list[Mapping[str, Any]],
client: MongoClient,
) -> Optional[Mapping[str, Any]]:
"""A proxy for Connection.unack_write that handles event publishing."""
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.STARTED,
command=cmd,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
)
if bwc.publish:
cmd = bwc._start(cmd, request_id, op_docs, ns_docs)
try:
result = bwc.conn.unack_write(msg, bwc.max_bson_size) # type: ignore[func-returns-value, misc, override]
duration = datetime.datetime.now() - bwc.start_time
if result is not None:
reply = _convert_write_result(bwc.name, cmd, result) # type: ignore[arg-type]
else:
# Comply with APM spec.
reply = {"ok": 1}
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.SUCCEEDED,
durationMS=duration,
reply=reply,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
)
if bwc.publish:
bwc._succeed(request_id, reply, duration)
except Exception as exc:
duration = datetime.datetime.now() - bwc.start_time
if isinstance(exc, OperationFailure):
failure: _DocumentOut = _convert_write_result(bwc.name, cmd, exc.details) # type: ignore[arg-type]
elif isinstance(exc, NotPrimaryError):
failure = exc.details # type: ignore[assignment]
else:
failure = _convert_exception(exc)
if _COMMAND_LOGGER.isEnabledFor(logging.DEBUG):
_debug_log(
_COMMAND_LOGGER,
clientId=client._topology_settings._topology_id,
message=_CommandStatusMessage.FAILED,
durationMS=duration,
failure=failure,
commandName=next(iter(cmd)),
databaseName=bwc.db_name,
requestId=request_id,
operationId=request_id,
driverConnectionId=bwc.conn.id,
serverConnectionId=bwc.conn.server_connection_id,
serverHost=bwc.conn.address[0],
serverPort=bwc.conn.address[1],
serviceId=bwc.conn.service_id,
isServerSideError=isinstance(exc, OperationFailure),
)
if bwc.publish:
assert bwc.start_time is not None
bwc._fail(request_id, failure, duration)
# Top-level error will be embedded in ClientBulkWriteException.
reply = {"error": exc}
finally:
bwc.start_time = datetime.datetime.now()
return result # type: ignore[return-value]
def _execute_batch_unack(
self,
bwc: _ClientBulkWriteContext,
cmd: dict[str, Any],
ops: list[tuple[str, Mapping[str, Any]]],
) -> tuple[list[Mapping[str, Any]], list[Mapping[str, Any]]]:
"""Executes a batch of bulkWrite server commands (unack)."""
request_id, msg, to_send_ops, to_send_ns = bwc.batch_command(cmd, ops)
self.unack_write(bwc, cmd, request_id, msg, to_send_ops, to_send_ns, self.client) # type: ignore[arg-type]
return to_send_ops, to_send_ns
def _execute_batch(
self,
bwc: _ClientBulkWriteContext,
cmd: dict[str, Any],
ops: list[tuple[str, Mapping[str, Any]]],
) -> tuple[dict[str, Any], list[Mapping[str, Any]], list[Mapping[str, Any]]]:
"""Executes a batch of bulkWrite server commands (ack)."""
request_id, msg, to_send_ops, to_send_ns = bwc.batch_command(cmd, ops)
result = self.write_command(bwc, cmd, request_id, msg, to_send_ops, to_send_ns, self.client) # type: ignore[arg-type]
self.client._process_response(result, bwc.session) # type: ignore[arg-type]
return result, to_send_ops, to_send_ns # type: ignore[return-value]
def _process_results_cursor(
self,
full_result: MutableMapping[str, Any],
result: MutableMapping[str, Any],
conn: Connection,
session: Optional[ClientSession],
) -> None:
"""Internal helper for processing the server reply command cursor."""
if result.get("cursor"):
coll = Collection(
database=Database(self.client, "admin"),
name="$cmd.bulkWrite",
)
cmd_cursor = CommandCursor(
coll,
result["cursor"],
conn.address,
session=session,
explicit_session=session is not None,
comment=self.comment,
)
cmd_cursor._maybe_pin_connection(conn)
# Iterate the cursor to get individual write results.
try:
for doc in cmd_cursor:
original_index = doc["idx"] + self.idx_offset
op_type, op = self.ops[original_index]
if not doc["ok"]:
result["writeErrors"].append(doc)
if self.ordered:
return
# Record individual write result.
if doc["ok"] and self.verbose_results:
if op_type == "insert":
inserted_id = op["document"]["_id"]
res = InsertOneResult(inserted_id, acknowledged=True) # type: ignore[assignment]
if op_type in ["update", "replace"]:
op_type = "update"
res = UpdateResult(doc, acknowledged=True, in_client_bulk=True) # type: ignore[assignment]
if op_type == "delete":
res = DeleteResult(doc, acknowledged=True) # type: ignore[assignment]
full_result[f"{op_type}Results"][original_index] = res
except Exception as exc:
# Attempt to close the cursor, then raise top-level error.
if cmd_cursor.alive:
cmd_cursor.close()
result["error"] = _convert_client_bulk_exception(exc)
def _execute_command(
self,
write_concern: WriteConcern,
session: Optional[ClientSession],
conn: Connection,
op_id: int,
retryable: bool,
full_result: MutableMapping[str, Any],
final_write_concern: Optional[WriteConcern] = None,
) -> None:
"""Internal helper for executing batches of bulkWrite commands."""
db_name = "admin"
cmd_name = "bulkWrite"
listeners = self.client._event_listeners
# Connection.command validates the session, but we use
# Connection.write_command
conn.validate_session(self.client, session)
bwc = self.bulk_ctx_class(
db_name,
cmd_name,
conn,
op_id,
listeners, # type: ignore[arg-type]
session,
self.client.codec_options,
)
while self.idx_offset < self.total_ops:
# If this is the last possible batch, use the
# final write concern.
if self.total_ops - self.idx_offset <= bwc.max_write_batch_size:
write_concern = final_write_concern or write_concern
# Construct the server command, specifying the relevant options.
cmd = {"bulkWrite": 1}
cmd["errorsOnly"] = not self.verbose_results
cmd["ordered"] = self.ordered # type: ignore[assignment]
not_in_transaction = session and not session.in_transaction
if not_in_transaction or not session:
_csot.apply_write_concern(cmd, write_concern)
if self.bypass_doc_val is not None:
cmd["bypassDocumentValidation"] = self.bypass_doc_val
if self.comment:
cmd["comment"] = self.comment # type: ignore[assignment]
if self.let:
cmd["let"] = self.let
if session:
# Start a new retryable write unless one was already
# started for this command.
if retryable and not self.started_retryable_write:
session._start_retryable_write()
self.started_retryable_write = True
session._apply_to(cmd, retryable, ReadPreference.PRIMARY, conn)
conn.send_cluster_time(cmd, session, self.client)
conn.add_server_api(cmd)
# CSOT: apply timeout before encoding the command.
conn.apply_timeout(self.client, cmd)
ops = islice(self.ops, self.idx_offset, None)
# Run as many ops as possible in one server command.
if write_concern.acknowledged:
raw_result, to_send_ops, _ = self._execute_batch(bwc, cmd, ops) # type: ignore[arg-type]
result = copy.deepcopy(raw_result)
# Top-level server/network error.
if result.get("error"):
error = result["error"]
retryable_top_level_error = (
isinstance(error.details, dict)
and error.details.get("code", 0) in _RETRYABLE_ERROR_CODES
)
retryable_network_error = isinstance(
error, ConnectionFailure
) and not isinstance(error, (NotPrimaryError, WaitQueueTimeoutError))
# Synthesize the full bulk result without modifying the
# current one because this write operation may be retried.
if retryable and (retryable_top_level_error or retryable_network_error):
full = copy.deepcopy(full_result)
_merge_command(self.ops, self.idx_offset, full, result)
_throw_client_bulk_write_exception(full, self.verbose_results)
else:
_merge_command(self.ops, self.idx_offset, full_result, result)
_throw_client_bulk_write_exception(full_result, self.verbose_results)
result["error"] = None
result["writeErrors"] = []
if result.get("nErrors", 0) < len(to_send_ops):
full_result["anySuccessful"] = True
# Top-level command error.
if not result["ok"]:
result["error"] = raw_result
_merge_command(self.ops, self.idx_offset, full_result, result)
break
if retryable:
# Retryable writeConcernErrors halt the execution of this batch.
wce = result.get("writeConcernError", {})
if wce.get("code", 0) in _RETRYABLE_ERROR_CODES:
# Synthesize the full bulk result without modifying the
# current one because this write operation may be retried.
full = copy.deepcopy(full_result)
_merge_command(self.ops, self.idx_offset, full, result)
_throw_client_bulk_write_exception(full, self.verbose_results)
# Process the server reply as a command cursor.
self._process_results_cursor(full_result, result, conn, session)
# Merge this batch's results with the full results.
_merge_command(self.ops, self.idx_offset, full_result, result)
# We're no longer in a retry once a command succeeds.
self.retrying = False
self.started_retryable_write = False
else:
to_send_ops, _ = self._execute_batch_unack(bwc, cmd, ops) # type: ignore[arg-type]
self.idx_offset += len(to_send_ops)
# We halt execution if we hit a top-level error,
# or an individual error in an ordered bulk write.
if full_result["error"] or (self.ordered and full_result["writeErrors"]):
break
def execute_command(
self,
session: Optional[ClientSession],
operation: str,
) -> MutableMapping[str, Any]:
"""Execute commands with w=1 WriteConcern."""
full_result: MutableMapping[str, Any] = {
"anySuccessful": False,
"error": None,
"writeErrors": [],
"writeConcernErrors": [],
"nInserted": 0,
"nUpserted": 0,
"nMatched": 0,
"nModified": 0,
"nDeleted": 0,
"insertResults": {},
"updateResults": {},
"deleteResults": {},
}
op_id = _randint()
def retryable_bulk(
session: Optional[ClientSession],
conn: Connection,
retryable: bool,
) -> None:
if conn.max_wire_version < 25:
raise InvalidOperation(
"MongoClient.bulk_write requires MongoDB server version 8.0+."
)
self._execute_command(
self.write_concern,
session,
conn,
op_id,
retryable,
full_result,
)
self.client._retryable_write(
self.is_retryable,
retryable_bulk,
session,
operation,
bulk=self,
operation_id=op_id,
)
if full_result["error"] or full_result["writeErrors"] or full_result["writeConcernErrors"]:
_throw_client_bulk_write_exception(full_result, self.verbose_results)
return full_result
def execute_command_unack_unordered(
self,
conn: Connection,
) -> None:
"""Execute commands with OP_MSG and w=0 writeConcern, unordered."""
db_name = "admin"
cmd_name = "bulkWrite"
listeners = self.client._event_listeners
op_id = _randint()
bwc = self.bulk_ctx_class(
db_name,
cmd_name,
conn,
op_id,
listeners, # type: ignore[arg-type]
None,
self.client.codec_options,
)
while self.idx_offset < self.total_ops:
# Construct the server command, specifying the relevant options.
cmd = {"bulkWrite": 1}
cmd["errorsOnly"] = not self.verbose_results
cmd["ordered"] = self.ordered # type: ignore[assignment]
if self.bypass_doc_val is not None:
cmd["bypassDocumentValidation"] = self.bypass_doc_val
cmd["writeConcern"] = {"w": 0} # type: ignore[assignment]
if self.comment:
cmd["comment"] = self.comment # type: ignore[assignment]
if self.let:
cmd["let"] = self.let
conn.add_server_api(cmd)
ops = islice(self.ops, self.idx_offset, None)
# Run as many ops as possible in one server command.
to_send_ops, _ = self._execute_batch_unack(bwc, cmd, ops) # type: ignore[arg-type]
self.idx_offset += len(to_send_ops)
def execute_command_unack_ordered(
self,
conn: Connection,
) -> None:
"""Execute commands with OP_MSG and w=0 WriteConcern, ordered."""
full_result: MutableMapping[str, Any] = {
"anySuccessful": False,
"error": None,
"writeErrors": [],
"writeConcernErrors": [],
"nInserted": 0,
"nUpserted": 0,
"nMatched": 0,
"nModified": 0,
"nDeleted": 0,
"insertResults": {},
"updateResults": {},
"deleteResults": {},
}
# Ordered bulk writes have to be acknowledged so that we stop
# processing at the first error, even when the application
# specified unacknowledged writeConcern.
initial_write_concern = WriteConcern()
op_id = _randint()
try:
self._execute_command(
initial_write_concern,
None,
conn,
op_id,
False,
full_result,
self.write_concern,
)
except OperationFailure:
pass
def execute_no_results(
self,
conn: Connection,
) -> None:
"""Execute all operations, returning no results (w=0)."""
if self.uses_collation:
raise ConfigurationError("Collation is unsupported for unacknowledged writes.")
if self.uses_array_filters:
raise ConfigurationError("arrayFilters is unsupported for unacknowledged writes.")
# Cannot have both unacknowledged writes and bypass document validation.
if self.bypass_doc_val is not None:
raise OperationFailure(
"Cannot set bypass_document_validation with unacknowledged write concern"
)
if self.ordered:
return self.execute_command_unack_ordered(conn)
return self.execute_command_unack_unordered(conn)
def execute(
self,
session: Optional[ClientSession],
operation: str,
) -> Any:
"""Execute operations."""
if not self.ops:
raise InvalidOperation("No operations to execute")
if self.executed:
raise InvalidOperation("Bulk operations can only be executed once.")
self.executed = True
session = _validate_session_write_concern(session, self.write_concern)
if not self.write_concern.acknowledged:
with self.client._conn_for_writes(session, operation) as connection:
if connection.max_wire_version < 25:
raise InvalidOperation(
"MongoClient.bulk_write requires MongoDB server version 8.0+."
)
self.execute_no_results(connection)
return ClientBulkWriteResult(None, False, False) # type: ignore[arg-type]
result = self.execute_command(session, operation)
return ClientBulkWriteResult(
result,
self.write_concern.acknowledged,
self.verbose_results,
)

View File

@ -23,11 +23,11 @@ Causally Consistent Reads
with client.start_session(causal_consistency=True) as session:
collection = client.db.collection
await collection.update_one({"_id": 1}, {"$set": {"x": 10}}, session=session)
collection.update_one({"_id": 1}, {"$set": {"x": 10}}, session=session)
secondary_c = collection.with_options(read_preference=ReadPreference.SECONDARY)
# A secondary read waits for replication of the write.
await secondary_c.find_one({"_id": 1}, session=session)
secondary_c.find_one({"_id": 1}, session=session)
If `causal_consistency` is True (the default), read operations that use
the session are causally after previous read and write operations. Using a
@ -54,15 +54,15 @@ operation:
orders = client.db.orders
inventory = client.db.inventory
with client.start_session() as session:
async with session.start_transaction():
await orders.insert_one({"sku": "abc123", "qty": 100}, session=session)
await inventory.update_one(
with session.start_transaction():
orders.insert_one({"sku": "abc123", "qty": 100}, session=session)
inventory.update_one(
{"sku": "abc123", "qty": {"$gte": 100}},
{"$inc": {"qty": -100}},
session=session,
)
Upon normal completion of ``async with session.start_transaction()`` block, the
Upon normal completion of ``with session.start_transaction()`` block, the
transaction automatically calls :meth:`ClientSession.commit_transaction`.
If the block exits with an exception, the transaction automatically calls
:meth:`ClientSession.abort_transaction`.
@ -114,8 +114,8 @@ replica set secondaries.
# Each read using this session reads data from the same point in time.
with client.start_session(snapshot=True) as session:
order = await orders.find_one({"sku": "abc123"}, session=session)
inventory = await inventory.find_one({"sku": "abc123"}, session=session)
order = orders.find_one({"sku": "abc123"}, session=session)
inventory = inventory.find_one({"sku": "abc123"}, session=session)
Snapshot Reads Limitations
^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -609,24 +609,24 @@ class ClientSession:
This method starts a transaction on this session, executes ``callback``
once, and then commits the transaction. For example::
async def callback(session):
def callback(session):
orders = session.client.db.orders
inventory = session.client.db.inventory
await orders.insert_one({"sku": "abc123", "qty": 100}, session=session)
await inventory.update_one({"sku": "abc123", "qty": {"$gte": 100}},
orders.insert_one({"sku": "abc123", "qty": 100}, session=session)
inventory.update_one({"sku": "abc123", "qty": {"$gte": 100}},
{"$inc": {"qty": -100}}, session=session)
with client.start_session() as session:
await session.with_transaction(callback)
session.with_transaction(callback)
To pass arbitrary arguments to the ``callback``, wrap your callable
with a ``lambda`` like this::
async def callback(session, custom_arg, custom_kwarg=None):
def callback(session, custom_arg, custom_kwarg=None):
# Transaction operations...
with client.start_session() as session:
await session.with_transaction(
session.with_transaction(
lambda s: callback(s, "custom_arg", custom_kwarg=1))
In the event of an exception, ``with_transaction`` may retry the commit

View File

@ -385,7 +385,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
__iter__ = None
def __next__(self) -> NoReturn:
raise TypeError(f"'{type(self).__name__}' object is not iterable")
raise TypeError("'Collection' object is not iterable")
next = __next__
@ -423,19 +423,19 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
Performs an aggregation with an implicit initial ``$changeStream``
stage and returns a
:class:`~pymongo.synchronous.change_stream.CollectionChangeStream` cursor which
:class:`~pymongo.change_stream.CollectionChangeStream` cursor which
iterates over changes on this collection.
.. code-block:: python
async with db.collection.watch() as stream:
async for change in stream:
with db.collection.watch() as stream:
for change in stream:
print(change)
The :class:`~pymongo.synchronous.change_stream.CollectionChangeStream` iterable
The :class:`~pymongo.change_stream.CollectionChangeStream` iterable
blocks until the next change document is returned or an error is
raised. If the
:meth:`~pymongo.synchronous.change_stream.CollectionChangeStream.next` method
:meth:`~pymongo.change_stream.CollectionChangeStream.next` method
encounters a network error when retrieving a batch from the server,
it will automatically attempt to recreate the cursor such that no
change events are missed. Any error encountered during the resume
@ -444,8 +444,8 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
.. code-block:: python
try:
async with db.collection.watch([{"$match": {"operationType": "insert"}}]) as stream:
async for insert_change in stream:
with db.coll.watch([{"$match": {"operationType": "insert"}}]) as stream:
for insert_change in stream:
print(insert_change)
except pymongo.errors.PyMongoError:
# The ChangeStream encountered an unrecoverable error or the
@ -502,7 +502,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
command.
:param show_expanded_events: Include expanded events such as DDL events like `dropIndexes`.
:return: A :class:`~pymongo.synchronous.change_stream.CollectionChangeStream` cursor.
:return: A :class:`~pymongo.change_stream.CollectionChangeStream` cursor.
.. versionchanged:: 4.3
Added `show_expanded_events` parameter.
@ -818,12 +818,12 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
) -> InsertOneResult:
"""Insert a single document.
>>> await db.test.count_documents({'x': 1})
>>> db.test.count_documents({'x': 1})
0
>>> result = await db.test.insert_one({'x': 1})
>>> result = db.test.insert_one({'x': 1})
>>> result.inserted_id
ObjectId('54f112defba522406c9cc208')
>>> await db.test.find_one({'x': 1})
>>> db.test.find_one({'x': 1})
{'x': 1, '_id': ObjectId('54f112defba522406c9cc208')}
:param document: The document to insert. Must be a mutable mapping
@ -884,12 +884,12 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
) -> InsertManyResult:
"""Insert an iterable of documents.
>>> await db.test.count_documents({})
>>> db.test.count_documents({})
0
>>> result = await db.test.insert_many([{'x': i} for i in range(2)])
>>> await result.inserted_ids
>>> result = db.test.insert_many([{'x': i} for i in range(2)])
>>> result.inserted_ids
[ObjectId('54f113fffba522406c9cc20e'), ObjectId('54f113fffba522406c9cc20f')]
>>> await db.test.count_documents({})
>>> db.test.count_documents({})
2
:param documents: A iterable of documents to insert.
@ -1098,16 +1098,16 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
) -> UpdateResult:
"""Replace a single document matching the filter.
>>> async for doc in db.test.find({}):
>>> for doc in db.test.find({}):
... print(doc)
...
{'x': 1, '_id': ObjectId('54f4c5befba5220aa4d6dee7')}
>>> result = await db.test.replace_one({'x': 1}, {'y': 1})
>>> result = db.test.replace_one({'x': 1}, {'y': 1})
>>> result.matched_count
1
>>> result.modified_count
1
>>> async for doc in db.test.find({}):
>>> for doc in db.test.find({}):
... print(doc)
...
{'y': 1, '_id': ObjectId('54f4c5befba5220aa4d6dee7')}
@ -1115,14 +1115,14 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
The *upsert* option can be used to insert a new document if a matching
document does not exist.
>>> result = await db.test.replace_one({'x': 1}, {'x': 1}, True)
>>> result = db.test.replace_one({'x': 1}, {'x': 1}, True)
>>> result.matched_count
0
>>> result.modified_count
0
>>> result.upserted_id
ObjectId('54f11e5c8891e756a6e1abd4')
>>> await db.test.find_one({'x': 1})
>>> db.test.find_one({'x': 1})
{'x': 1, '_id': ObjectId('54f11e5c8891e756a6e1abd4')}
:param filter: A query that matches the document to replace.
@ -1201,18 +1201,18 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
) -> UpdateResult:
"""Update a single document matching the filter.
>>> async for doc in db.test.find():
>>> for doc in db.test.find():
... print(doc)
...
{'x': 1, '_id': 0}
{'x': 1, '_id': 1}
{'x': 1, '_id': 2}
>>> result = await db.test.update_one({'x': 1}, {'$inc': {'x': 3}})
>>> result = db.test.update_one({'x': 1}, {'$inc': {'x': 3}})
>>> result.matched_count
1
>>> result.modified_count
1
>>> async for doc in db.test.find():
>>> for doc in db.test.find():
... print(doc)
...
{'x': 4, '_id': 0}
@ -1222,14 +1222,14 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
If ``upsert=True`` and no documents match the filter, create a
new document based on the filter criteria and update modifications.
>>> result = await db.test.update_one({'x': -10}, {'$inc': {'x': 3}}, upsert=True)
>>> result = db.test.update_one({'x': -10}, {'$inc': {'x': 3}}, upsert=True)
>>> result.matched_count
0
>>> result.modified_count
0
>>> result.upserted_id
ObjectId('626a678eeaa80587d4bb3fb7')
>>> await db.test.find_one(result.upserted_id)
>>> db.test.find_one(result.upserted_id)
{'_id': ObjectId('626a678eeaa80587d4bb3fb7'), 'x': -7}
:param filter: A query that matches the document to update.
@ -1314,18 +1314,18 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
) -> UpdateResult:
"""Update one or more documents that match the filter.
>>> async for doc in db.test.find():
>>> for doc in db.test.find():
... print(doc)
...
{'x': 1, '_id': 0}
{'x': 1, '_id': 1}
{'x': 1, '_id': 2}
>>> result = await db.test.update_many({'x': 1}, {'$inc': {'x': 3}})
>>> result = db.test.update_many({'x': 1}, {'$inc': {'x': 3}})
>>> result.matched_count
3
>>> result.modified_count
3
>>> async for doc in db.test.find():
>>> for doc in db.test.find():
... print(doc)
...
{'x': 4, '_id': 0}
@ -1417,8 +1417,8 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
The following two calls are equivalent:
>>> await db.foo.drop()
>>> await db.drop_collection("foo")
>>> db.foo.drop()
>>> db.drop_collection("foo")
.. versionchanged:: 4.2
Added ``encrypted_fields`` parameter.
@ -1550,12 +1550,12 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
) -> DeleteResult:
"""Delete a single document matching the filter.
>>> await db.test.count_documents({'x': 1})
>>> db.test.count_documents({'x': 1})
3
>>> result = await db.test.delete_one({'x': 1})
>>> result = db.test.delete_one({'x': 1})
>>> result.deleted_count
1
>>> await db.test.count_documents({'x': 1})
>>> db.test.count_documents({'x': 1})
2
:param filter: A query that matches the document to delete.
@ -1615,12 +1615,12 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
) -> DeleteResult:
"""Delete one or more documents matching the filter.
>>> await db.test.count_documents({'x': 1})
>>> db.test.count_documents({'x': 1})
3
>>> result = await db.test.delete_many({'x': 1})
>>> result = db.test.delete_many({'x': 1})
>>> result.deleted_count
3
>>> await db.test.count_documents({'x': 1})
>>> db.test.count_documents({'x': 1})
0
:param filter: A query that matches the documents to delete.
@ -1694,7 +1694,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
:: code-block: python
>>> await collection.find_one(max_time_ms=100)
>>> collection.find_one(max_time_ms=100)
"""
if filter is not None and not isinstance(filter, abc.Mapping):
@ -1904,8 +1904,8 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
:mod:`bson` module.
>>> import bson
>>> cursor = await db.test.find_raw_batches()
>>> async for batch in cursor:
>>> cursor = db.test.find_raw_batches()
>>> for batch in cursor:
... print(bson.decode_all(batch))
.. note:: find_raw_batches does not support auto encryption.
@ -2133,7 +2133,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
>>> index1 = IndexModel([("hello", DESCENDING),
... ("world", ASCENDING)], name="hello_world")
>>> index2 = IndexModel([("goodbye", DESCENDING)])
>>> await db.test.create_indexes([index1, index2])
>>> db.test.create_indexes([index1, index2])
["hello_world", "goodbye_-1"]
:param indexes: A list of :class:`~pymongo.operations.IndexModel`
@ -2232,18 +2232,18 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
To create a single key ascending index on the key ``'mike'`` we just
use a string argument::
>>> await my_collection.create_index("mike")
>>> my_collection.create_index("mike")
For a compound index on ``'mike'`` descending and ``'eliot'``
ascending we need to use a list of tuples::
>>> await my_collection.create_index([("mike", pymongo.DESCENDING),
>>> my_collection.create_index([("mike", pymongo.DESCENDING),
... "eliot"])
All optional index creation parameters should be passed as
keyword arguments to this method. For example::
>>> await my_collection.create_index([("mike", pymongo.DESCENDING)],
>>> my_collection.create_index([("mike", pymongo.DESCENDING)],
... background=True)
Valid options include, but are not limited to:
@ -2448,7 +2448,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
) -> CommandCursor[MutableMapping[str, Any]]:
"""Get a cursor over the index documents for this collection.
>>> async for index in db.test.list_indexes():
>>> for index in db.test.list_indexes():
... print(index)
...
SON([('v', 2), ('key', SON([('_id', 1)])), ('name', '_id_')])
@ -2957,9 +2957,9 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
:mod:`bson` module.
>>> import bson
>>> cursor = await db.test.aggregate_raw_batches([
>>> cursor = db.test.aggregate_raw_batches([
... {'$project': {'x': {'$multiply': [2, '$x']}}}])
>>> async for batch in cursor:
>>> for batch in cursor:
... print(bson.decode_all(batch))
.. note:: aggregate_raw_batches does not support auto encryption.
@ -3217,28 +3217,28 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
) -> _DocumentType:
"""Finds a single document and deletes it, returning the document.
>>> await db.test.count_documents({'x': 1})
>>> db.test.count_documents({'x': 1})
2
>>> await db.test.find_one_and_delete({'x': 1})
>>> db.test.find_one_and_delete({'x': 1})
{'x': 1, '_id': ObjectId('54f4e12bfba5220aa4d6dee8')}
>>> await db.test.count_documents({'x': 1})
>>> db.test.count_documents({'x': 1})
1
If multiple documents match *filter*, a *sort* can be applied.
>>> async for doc in db.test.find({'x': 1}):
>>> for doc in db.test.find({'x': 1}):
... print(doc)
...
{'x': 1, '_id': 0}
{'x': 1, '_id': 1}
{'x': 1, '_id': 2}
>>> await db.test.find_one_and_delete(
>>> db.test.find_one_and_delete(
... {'x': 1}, sort=[('_id', pymongo.DESCENDING)])
{'x': 1, '_id': 2}
The *projection* option can be used to limit the fields returned.
>>> await db.test.find_one_and_delete({'x': 1}, projection={'_id': False})
>>> db.test.find_one_and_delete({'x': 1}, projection={'_id': False})
{'x': 1}
:param filter: A query that matches the document to delete.
@ -3314,15 +3314,15 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
:meth:`find_one_and_update` by replacing the document matched by
*filter*, rather than modifying the existing document.
>>> async for doc in db.test.find({}):
>>> for doc in db.test.find({}):
... print(doc)
...
{'x': 1, '_id': 0}
{'x': 1, '_id': 1}
{'x': 1, '_id': 2}
>>> await db.test.find_one_and_replace({'x': 1}, {'y': 1})
>>> db.test.find_one_and_replace({'x': 1}, {'y': 1})
{'x': 1, '_id': 0}
>>> async for doc in db.test.find({}):
>>> for doc in db.test.find({}):
... print(doc)
...
{'y': 1, '_id': 0}
@ -3418,13 +3418,13 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
"""Finds a single document and updates it, returning either the
original or the updated document.
>>> await db.test.find_one_and_update(
>>> db.test.find_one_and_update(
... {'_id': 665}, {'$inc': {'count': 1}, '$set': {'done': True}})
{'_id': 665, 'done': False, 'count': 25}}
Returns ``None`` if no document matches the filter.
>>> await db.test.find_one_and_update(
>>> db.test.find_one_and_update(
... {'_exists': False}, {'$inc': {'count': 1}})
When the filter matches, by default :meth:`find_one_and_update`
@ -3434,7 +3434,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
option.
>>> from pymongo import ReturnDocument
>>> await db.example.find_one_and_update(
>>> db.example.find_one_and_update(
... {'_id': 'userid'},
... {'$inc': {'seq': 1}},
... return_document=ReturnDocument.AFTER)
@ -3442,7 +3442,7 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
You can limit the fields returned with the *projection* option.
>>> await db.example.find_one_and_update(
>>> db.example.find_one_and_update(
... {'_id': 'userid'},
... {'$inc': {'seq': 1}},
... projection={'seq': True, '_id': False},
@ -3452,9 +3452,9 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
The *upsert* option can be used to create the document if it doesn't
already exist.
>>> await db.example.delete_many({}).deleted_count
>>> (db.example.delete_many({})).deleted_count
1
>>> await db.example.find_one_and_update(
>>> db.example.find_one_and_update(
... {'_id': 'userid'},
... {'$inc': {'seq': 1}},
... projection={'seq': True, '_id': False},
@ -3464,12 +3464,12 @@ class Collection(common.BaseObject, Generic[_DocumentType]):
If multiple documents match *filter*, a *sort* can be applied.
>>> async for doc in db.test.find({'done': True}):
>>> for doc in db.test.find({'done': True}):
... print(doc)
...
{'_id': 665, 'done': True, 'result': {'count': 26}}
{'_id': 701, 'done': True, 'result': {'count': 17}}
>>> await db.test.find_one_and_update(
>>> db.test.find_one_and_update(
... {'done': True},
... {'$set': {'final': True}},
... sort=[('_id', pymongo.DESCENDING)])

View File

@ -164,7 +164,7 @@ class CommandCursor(Generic[_DocumentType]):
Even if :attr:`alive` is ``True``, :meth:`next` can raise
:exc:`StopIteration`. Best to use a for loop::
async for doc in collection.aggregate(pipeline):
for doc in collection.aggregate(pipeline):
print(doc)
.. note:: :attr:`alive` can be True while iterating a cursor from
@ -382,11 +382,11 @@ class CommandCursor(Generic[_DocumentType]):
self.close()
def to_list(self) -> list[_DocumentType]:
"""Converts the contents of this cursor to a list more efficiently than ``[doc async for doc in cursor]``.
"""Converts the contents of this cursor to a list more efficiently than ``[doc for doc in cursor]``.
To use::
>>> await cursor.to_list()
>>> cursor.to_list()
If the cursor is empty or has no more results, an empty list will be returned.

View File

@ -527,7 +527,7 @@ class Cursor(Generic[_DocumentType]):
def max_await_time_ms(self, max_await_time_ms: Optional[int]) -> Cursor[_DocumentType]:
"""Specifies a time limit for a getMore operation on a
:attr:`~pymongo.cursor_shared.CursorType.TAILABLE_AWAIT` cursor. For all other
:attr:`~pymongo.cursor.CursorType.TAILABLE_AWAIT` cursor. For all other
types of cursor max_await_time_ms is ignored.
Raises :exc:`TypeError` if `max_await_time_ms` is not an integer or
@ -712,27 +712,27 @@ class Cursor(Generic[_DocumentType]):
Pass a field name and a direction, either
:data:`~pymongo.ASCENDING` or :data:`~pymongo.DESCENDING`.::
async for doc in collection.find().sort('field', pymongo.ASCENDING):
for doc in collection.find().sort('field', pymongo.ASCENDING):
print(doc)
To sort by multiple fields, pass a list of (key, direction) pairs.
If just a name is given, :data:`~pymongo.ASCENDING` will be inferred::
async for doc in collection.find().sort([
for doc in collection.find().sort([
'field1',
('field2', pymongo.DESCENDING)]):
print(doc)
Text search results can be sorted by relevance::
cursor = await db.test.find(
cursor = db.test.find(
{'$text': {'$search': 'some words'}},
{'score': {'$meta': 'textScore'}})
# Sort by 'score' field.
cursor.sort([('score', {'$meta': 'textScore'})])
async for doc in cursor:
for doc in cursor:
print(doc)
For more advanced text search functionality, see MongoDB's
@ -831,7 +831,7 @@ class Cursor(Generic[_DocumentType]):
to the object currently being scanned. For example::
# Find all documents where field "a" is less than "b" plus "c".
async for doc in db.test.find().where('this.a < (this.b + this.c)'):
for doc in db.test.find().where('this.a < (this.b + this.c)'):
print(doc)
Raises :class:`TypeError` if `code` is not an instance of
@ -904,7 +904,7 @@ class Cursor(Generic[_DocumentType]):
With regular cursors, simply use a for loop instead of :attr:`alive`::
async for doc in collection.find():
for doc in collection.find():
print(doc)
.. note:: Even if :attr:`alive` is True, :meth:`next` can raise
@ -1285,11 +1285,11 @@ class Cursor(Generic[_DocumentType]):
self.close()
def to_list(self) -> list[_DocumentType]:
"""Converts the contents of this cursor to a list more efficiently than ``[doc async for doc in cursor]``.
"""Converts the contents of this cursor to a list more efficiently than ``[doc for doc in cursor]``.
To use::
>>> await cursor.to_list()
>>> cursor.to_list()
If the cursor is empty or has no more results, an empty list will be returned.

View File

@ -337,21 +337,21 @@ class Database(common.BaseObject, Generic[_DocumentType]):
Performs an aggregation with an implicit initial ``$changeStream``
stage and returns a
:class:`~pymongo.synchronous.change_stream.DatabaseChangeStream` cursor which
:class:`~pymongo.change_stream.DatabaseChangeStream` cursor which
iterates over changes on all collections in this database.
Introduced in MongoDB 4.0.
.. code-block:: python
async with db.watch() as stream:
async for change in stream:
with db.watch() as stream:
for change in stream:
print(change)
The :class:`~pymongo.synchronous.change_stream.DatabaseChangeStream` iterable
The :class:`~pymongo.change_stream.DatabaseChangeStream` iterable
blocks until the next change document is returned or an error is
raised. If the
:meth:`~pymongo.synchronous.change_stream.DatabaseChangeStream.next` method
:meth:`~pymongo.change_stream.DatabaseChangeStream.next` method
encounters a network error when retrieving a batch from the server,
it will automatically attempt to recreate the cursor such that no
change events are missed. Any error encountered during the resume
@ -360,8 +360,8 @@ class Database(common.BaseObject, Generic[_DocumentType]):
.. code-block:: python
try:
async with db.watch([{"$match": {"operationType": "insert"}}]) as stream:
async for insert_change in stream:
with db.watch([{"$match": {"operationType": "insert"}}]) as stream:
for insert_change in stream:
print(insert_change)
except pymongo.errors.PyMongoError:
# The ChangeStream encountered an unrecoverable error or the
@ -409,7 +409,7 @@ class Database(common.BaseObject, Generic[_DocumentType]):
command.
:param show_expanded_events: Include expanded events such as DDL events like `dropIndexes`.
:return: A :class:`~pymongo.synchronous.change_stream.DatabaseChangeStream` cursor.
:return: A :class:`~pymongo.change_stream.DatabaseChangeStream` cursor.
.. versionchanged:: 4.3
Added `show_expanded_events` parameter.
@ -810,23 +810,23 @@ class Database(common.BaseObject, Generic[_DocumentType]):
For example, a command like ``{buildinfo: 1}`` can be sent
using:
>>> await db.command("buildinfo")
>>> db.command("buildinfo")
OR
>>> await db.command({"buildinfo": 1})
>>> db.command({"buildinfo": 1})
For a command where the value matters, like ``{count:
collection_name}`` we can do:
>>> await db.command("count", collection_name)
>>> db.command("count", collection_name)
OR
>>> await db.command({"count": collection_name})
>>> db.command({"count": collection_name})
For commands that take additional arguments we can use
kwargs. So ``{count: collection_name, query: query}`` becomes:
>>> await db.command("count", collection_name, query=query)
>>> db.command("count", collection_name, query=query)
OR
>>> await db.command({"count": collection_name, "query": query})
>>> db.command({"count": collection_name, "query": query})
:param command: document representing the command to be issued,
or the name of the command (for simple commands only).

View File

@ -62,6 +62,7 @@ from pymongo.client_options import ClientOptions
from pymongo.errors import (
AutoReconnect,
BulkWriteError,
ClientBulkWriteException,
ConfigurationError,
ConnectionFailure,
InvalidOperation,
@ -76,12 +77,22 @@ from pymongo.lock import _HAS_REGISTER_AT_FORK, _create_lock, _release_locks
from pymongo.logger import _CLIENT_LOGGER, _log_or_warn
from pymongo.message import _CursorAddress, _GetMore, _Query
from pymongo.monitoring import ConnectionClosedReason
from pymongo.operations import _Op
from pymongo.operations import (
DeleteMany,
DeleteOne,
InsertOne,
ReplaceOne,
UpdateMany,
UpdateOne,
_Op,
)
from pymongo.read_preferences import ReadPreference, _ServerMode
from pymongo.results import ClientBulkWriteResult
from pymongo.server_selectors import writable_server_selector
from pymongo.server_type import SERVER_TYPE
from pymongo.synchronous import client_session, database, periodic_executor
from pymongo.synchronous.change_stream import ChangeStream, ClusterChangeStream
from pymongo.synchronous.client_bulk import _ClientBulk
from pymongo.synchronous.client_session import _EmptyServerSession
from pymongo.synchronous.command_cursor import CommandCursor
from pymongo.synchronous.settings import TopologySettings
@ -127,6 +138,15 @@ _ReadCall = Callable[
_IS_SYNC = True
_WriteOp = Union[
InsertOne,
DeleteOne,
DeleteMany,
ReplaceOne,
UpdateOne,
UpdateMany,
]
class MongoClient(common.BaseObject, Generic[_DocumentType]):
HOST = "localhost"
@ -1715,7 +1735,7 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
retryable: bool,
func: _WriteCall[T],
session: Optional[ClientSession],
bulk: Optional[_Bulk],
bulk: Optional[Union[_Bulk, _ClientBulk]],
operation: str,
operation_id: Optional[int] = None,
) -> T:
@ -1745,7 +1765,7 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
self,
func: _WriteCall[T] | _ReadCall[T],
session: Optional[ClientSession],
bulk: Optional[_Bulk],
bulk: Optional[Union[_Bulk, _ClientBulk]],
operation: str,
is_read: bool = False,
address: Optional[_Address] = None,
@ -1828,7 +1848,7 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
func: _WriteCall[T],
session: Optional[ClientSession],
operation: str,
bulk: Optional[_Bulk] = None,
bulk: Optional[Union[_Bulk, _ClientBulk]] = None,
operation_id: Optional[int] = None,
) -> T:
"""Execute an operation with consecutive retries if possible
@ -2193,10 +2213,134 @@ class MongoClient(common.BaseObject, Generic[_DocumentType]):
session=session,
)
@_csot.apply
def bulk_write(
self,
models: Sequence[_WriteOp[_DocumentType]],
session: Optional[ClientSession] = None,
ordered: bool = True,
verbose_results: bool = False,
bypass_document_validation: Optional[bool] = None,
comment: Optional[Any] = None,
let: Optional[Mapping] = None,
write_concern: Optional[WriteConcern] = None,
) -> ClientBulkWriteResult:
"""Send a batch of write operations, potentially across multiple namespaces, to the server.
Requests are passed as a list of write operation instances (
:class:`~pymongo.operations.InsertOne`,
:class:`~pymongo.operations.UpdateOne`,
:class:`~pymongo.operations.UpdateMany`,
:class:`~pymongo.operations.ReplaceOne`,
:class:`~pymongo.operations.DeleteOne`, or
:class:`~pymongo.operations.DeleteMany`).
>>> for doc in db.test.find({}):
... print(doc)
...
{'x': 1, '_id': ObjectId('54f62e60fba5226811f634ef')}
{'x': 1, '_id': ObjectId('54f62e60fba5226811f634f0')}
...
>>> for doc in db.coll.find({}):
... print(doc)
...
{'x': 2, '_id': ObjectId('507f1f77bcf86cd799439011')}
...
>>> # DeleteMany, UpdateOne, and UpdateMany are also available.
>>> from pymongo import InsertOne, DeleteOne, ReplaceOne
>>> models = [InsertOne(namespace="db.test", document={'y': 1}),
... DeleteOne(namespace="db.test", filter={'x': 1}),
... InsertOne(namespace="db.coll", document={'y': 2}),
... ReplaceOne(namespace="db.test", filter={'w': 1}, replacement={'z': 1}, upsert=True)]
>>> result = client.bulk_write(models=models)
>>> result.inserted_count
2
>>> result.deleted_count
1
>>> result.modified_count
0
>>> result.upserted_ids
{3: ObjectId('54f62ee28891e756a6e1abd5')}
>>> for doc in db.test.find({}):
... print(doc)
...
{'x': 1, '_id': ObjectId('54f62e60fba5226811f634f0')}
{'y': 1, '_id': ObjectId('54f62ee2fba5226811f634f1')}
{'z': 1, '_id': ObjectId('54f62ee28891e756a6e1abd5')}
...
>>> for doc in db.coll.find({}):
... print(doc)
...
{'x': 2, '_id': ObjectId('507f1f77bcf86cd799439011')}
{'y': 2, '_id': ObjectId('507f1f77bcf86cd799439012')}
:param models: A list of write operation instances.
:param session: (optional) An instance of
:class:`~pymongo.client_session.ClientSession`.
:param ordered: If ``True`` (the default), requests will be
performed on the server serially, in the order provided. If an error
occurs all remaining operations are aborted. If ``False``, requests
will be still performed on the server serially, in the order provided,
but all operations will be attempted even if any errors occur.
:param verbose_results: If ``True``, detailed results for each
successful operation will be included in the returned
:class:`~pymongo.results.ClientBulkWriteResult`. Default is ``False``.
:param bypass_document_validation: (optional) If ``True``, allows the
write to opt-out of document level validation. Default is ``False``.
:param comment: (optional) A user-provided comment to attach to this
command.
:param let: (optional) Map of parameter names and values. Values must be
constant or closed expressions that do not reference document
fields. Parameters can then be accessed as variables in an
aggregate expression context (e.g. "$$var").
:param write_concern: (optional) The write concern to use for this bulk write.
:return: An instance of :class:`~pymongo.results.ClientBulkWriteResult`.
.. seealso:: :ref:`writes-and-ids`
.. note:: requires MongoDB server version 8.0+.
.. versionadded:: 4.9
"""
if self._options.auto_encryption_opts:
raise InvalidOperation(
"MongoClient.bulk_write does not currently support automatic encryption"
)
if session and session.in_transaction:
# Inherit the transaction write concern.
if write_concern:
raise InvalidOperation("Cannot set write concern after starting a transaction")
write_concern = session._transaction.opts.write_concern # type: ignore[union-attr]
else:
# Inherit the client's write concern if none is provided.
if not write_concern:
write_concern = self.write_concern
common.validate_list("models", models)
blk = _ClientBulk(
self,
write_concern=write_concern, # type: ignore[arg-type]
ordered=ordered,
bypass_document_validation=bypass_document_validation,
comment=comment,
let=let,
verbose_results=verbose_results,
)
for model in models:
try:
model._add_to_client_bulk(blk)
except AttributeError:
raise TypeError(f"{model!r} is not a valid request") from None
return blk.execute(session, _Op.BULK_WRITE)
def _retryable_error_doc(exc: PyMongoError) -> Optional[Mapping[str, Any]]:
"""Return the server response from PyMongo exception or None."""
if isinstance(exc, BulkWriteError):
if isinstance(exc, (BulkWriteError, ClientBulkWriteException)):
# Check the last writeConcernError to determine if this
# BulkWriteError is retryable.
wces = exc.details["writeConcernErrors"]
@ -2231,10 +2375,14 @@ def _add_retryable_write_error(exc: PyMongoError, max_wire_version: int, is_mong
# Connection errors are always retryable except NotPrimaryError and WaitQueueTimeoutError which is
# handled above.
if isinstance(exc, ConnectionFailure) and not isinstance(
exc, (NotPrimaryError, WaitQueueTimeoutError)
if isinstance(exc, ClientBulkWriteException):
exc_to_check = exc.error
else:
exc_to_check = exc
if isinstance(exc_to_check, ConnectionFailure) and not isinstance(
exc_to_check, (NotPrimaryError, WaitQueueTimeoutError)
):
exc._add_error_label("RetryableWriteError")
exc_to_check._add_error_label("RetryableWriteError")
class _MongoClientErrorHandler:
@ -2279,6 +2427,8 @@ class _MongoClientErrorHandler:
return
self.handled = True
if self.session:
if isinstance(exc_val, ClientBulkWriteException):
exc_val = exc_val.error
if isinstance(exc_val, ConnectionFailure):
if self.session.in_transaction:
exc_val._add_error_label("TransientTransactionError")
@ -2290,7 +2440,7 @@ class _MongoClientErrorHandler:
):
self.session._unpin()
err_ctx = _ErrorContext(
exc_val,
exc_val, # type: ignore[arg-type]
self.max_wire_version,
self.sock_generation,
self.completed_handshake,
@ -2317,7 +2467,7 @@ class _ClientConnectionRetryable(Generic[T]):
self,
mongo_client: MongoClient,
func: _WriteCall[T] | _ReadCall[T],
bulk: Optional[_Bulk],
bulk: Optional[Union[_Bulk, _ClientBulk]],
operation: str,
is_read: bool = False,
session: Optional[ClientSession] = None,
@ -2394,7 +2544,10 @@ class _ClientConnectionRetryable(Generic[T]):
if not self._is_read:
if not self._retryable:
raise
retryable_write_error_exc = exc.has_error_label("RetryableWriteError")
if isinstance(exc, ClientBulkWriteException) and exc.error:
retryable_write_error_exc = exc.error.has_error_label("RetryableWriteError")
else:
retryable_write_error_exc = exc.has_error_label("RetryableWriteError")
if retryable_write_error_exc:
assert self._session
self._session._unpin()

View File

@ -48,6 +48,15 @@ def _sanitize(error: Exception) -> None:
error.__cause__ = None
def _monotonic_duration(start: float) -> float:
"""Return the duration since the given start time.
Accounts for buggy platforms where time.monotonic() is not monotonic.
See PYTHON-4600.
"""
return max(0.0, time.monotonic() - start)
class MonitorBase:
def __init__(self, topology: Topology, name: str, interval: int, min_interval: float):
"""Base class to do periodic work on a background thread.
@ -247,7 +256,7 @@ class Monitor(MonitorBase):
_sanitize(error)
sd = self._server_description
address = sd.address
duration = time.monotonic() - start
duration = _monotonic_duration(start)
if self._publish:
awaited = bool(self._stream and sd.is_server_type_known and sd.topology_version)
assert self._listeners is not None
@ -317,7 +326,8 @@ class Monitor(MonitorBase):
else:
# New connection handshake or polling hello (MongoDB <4.4).
response = conn._hello(cluster_time, None, None)
return response, time.monotonic() - start
duration = _monotonic_duration(start)
return response, duration
class SrvMonitor(MonitorBase):
@ -441,7 +451,7 @@ class _RttMonitor(MonitorBase):
raise Exception("_RttMonitor closed")
start = time.monotonic()
conn.hello()
return time.monotonic() - start
return _monotonic_duration(start)
# Close monitors to cancel any in progress streaming checks before joining

View File

@ -643,6 +643,7 @@ class Topology:
:exc:`~.errors.InvalidOperation`.
"""
with self._lock:
old_td = self._description
for server in self._servers.values():
server.close()
@ -662,9 +663,30 @@ class Topology:
# Publish only after releasing the lock.
if self._publish_tp:
assert self._events is not None
self._description = TopologyDescription(
TOPOLOGY_TYPE.Unknown,
{},
self._description.replica_set_name,
self._description.max_set_version,
self._description.max_election_id,
self._description._topology_settings,
)
self._events.put(
(
self._listeners.publish_topology_description_changed,
(
old_td,
self._description,
self._topology_id,
),
)
)
self._events.put((self._listeners.publish_topology_closed, (self._topology_id,)))
if self._publish_server or self._publish_tp:
# Make sure the events executor thread is fully closed before publishing the remaining events
self.__events_executor.close()
self.__events_executor.join(1)
process_events_queue(weakref.ref(self._events)) # type: ignore[arg-type]
@property
def description(self) -> TopologyDescription:

View File

@ -30,11 +30,13 @@ from bson.typings import _DocumentOut, _DocumentType, _DocumentTypeArg
if TYPE_CHECKING:
from pymongo.asynchronous.bulk import _AsyncBulk
from pymongo.asynchronous.client_bulk import _AsyncClientBulk
from pymongo.asynchronous.client_session import AsyncClientSession
from pymongo.asynchronous.mongo_client import AsyncMongoClient
from pymongo.asynchronous.pool import AsyncConnection
from pymongo.collation import Collation
from pymongo.synchronous.bulk import _Bulk
from pymongo.synchronous.client_bulk import _ClientBulk
from pymongo.synchronous.client_session import ClientSession
from pymongo.synchronous.mongo_client import MongoClient
from pymongo.synchronous.pool import Connection
@ -53,6 +55,7 @@ _AgnosticMongoClient = Union["AsyncMongoClient", "MongoClient"]
_AgnosticConnection = Union["AsyncConnection", "Connection"]
_AgnosticClientSession = Union["AsyncClientSession", "ClientSession"]
_AgnosticBulk = Union["_AsyncBulk", "_Bulk"]
_AgnosticClientBulk = Union["_AsyncClientBulk", "_ClientBulk"]
def strip_optional(elem: Optional[_T]) -> _T:

View File

@ -2,4 +2,5 @@ sphinx>=5.3,<8
sphinx_rtd_theme>=2,<3
readthedocs-sphinx-search~=0.3
sphinxcontrib-shellcheck>=1,<2
sphinx-autobuild>=2020.9.1
furo==2023.9.10

View File

@ -0,0 +1,571 @@
# Copyright 2024-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Test the client bulk write API."""
from __future__ import annotations
import sys
sys.path[0:0] = [""]
from test.asynchronous import AsyncIntegrationTest, async_client_context, unittest
from test.utils import (
OvertCommandListener,
async_rs_or_single_client,
)
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT, AutoEncryptionOpts
from pymongo.errors import (
ClientBulkWriteException,
DocumentTooLarge,
InvalidOperation,
NetworkTimeout,
)
from pymongo.monitoring import *
from pymongo.operations import *
from pymongo.write_concern import WriteConcern
_IS_SYNC = False
class TestClientBulkWrite(AsyncIntegrationTest):
@async_client_context.require_version_min(8, 0, 0, -24)
async def test_returns_error_if_no_namespace_provided(self):
client = await async_rs_or_single_client()
self.addAsyncCleanup(client.aclose)
models = [InsertOne(document={"a": "b"})]
with self.assertRaises(InvalidOperation) as context:
await client.bulk_write(models=models)
self.assertIn(
"MongoClient.bulk_write requires a namespace to be provided for each write operation",
context.exception._message,
)
# https://github.com/mongodb/specifications/tree/master/source/crud/tests
class TestClientBulkWriteCRUD(AsyncIntegrationTest):
@async_client_context.require_version_min(8, 0, 0, -24)
async def test_batch_splits_if_num_operations_too_large(self):
listener = OvertCommandListener()
client = await async_rs_or_single_client(event_listeners=[listener])
self.addAsyncCleanup(client.aclose)
max_write_batch_size = (await async_client_context.hello)["maxWriteBatchSize"]
models = []
for _ in range(max_write_batch_size + 1):
models.append(InsertOne(namespace="db.coll", document={"a": "b"}))
self.addAsyncCleanup(client.db["coll"].drop)
result = await client.bulk_write(models=models)
self.assertEqual(result.inserted_count, max_write_batch_size + 1)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)
first_event, second_event = bulk_write_events
self.assertEqual(len(first_event.command["ops"]), max_write_batch_size)
self.assertEqual(len(second_event.command["ops"]), 1)
self.assertEqual(first_event.operation_id, second_event.operation_id)
@async_client_context.require_version_min(8, 0, 0, -24)
async def test_batch_splits_if_ops_payload_too_large(self):
listener = OvertCommandListener()
client = await async_rs_or_single_client(event_listeners=[listener])
self.addAsyncCleanup(client.aclose)
max_message_size_bytes = (await async_client_context.hello)["maxMessageSizeBytes"]
max_bson_object_size = (await async_client_context.hello)["maxBsonObjectSize"]
models = []
num_models = int(max_message_size_bytes / max_bson_object_size + 1)
b_repeated = "b" * (max_bson_object_size - 500)
for _ in range(num_models):
models.append(
InsertOne(
namespace="db.coll",
document={"a": b_repeated},
)
)
self.addAsyncCleanup(client.db["coll"].drop)
result = await client.bulk_write(models=models)
self.assertEqual(result.inserted_count, num_models)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)
first_event, second_event = bulk_write_events
self.assertEqual(len(first_event.command["ops"]), num_models - 1)
self.assertEqual(len(second_event.command["ops"]), 1)
self.assertEqual(first_event.operation_id, second_event.operation_id)
@async_client_context.require_version_min(8, 0, 0, -24)
@async_client_context.require_failCommand_fail_point
async def test_collects_write_concern_errors_across_batches(self):
listener = OvertCommandListener()
client = await async_rs_or_single_client(
event_listeners=[listener],
retryWrites=False,
)
self.addAsyncCleanup(client.aclose)
max_write_batch_size = (await async_client_context.hello)["maxWriteBatchSize"]
fail_command = {
"configureFailPoint": "failCommand",
"mode": {"times": 2},
"data": {
"failCommands": ["bulkWrite"],
"writeConcernError": {"code": 91, "errmsg": "Replication is being shut down"},
},
}
async with self.fail_point(fail_command):
models = []
for _ in range(max_write_batch_size + 1):
models.append(
InsertOne(
namespace="db.coll",
document={"a": "b"},
)
)
self.addAsyncCleanup(client.db["coll"].drop)
with self.assertRaises(ClientBulkWriteException) as context:
await client.bulk_write(models=models)
self.assertEqual(len(context.exception.write_concern_errors), 2) # type: ignore[arg-type]
self.assertIsNotNone(context.exception.partial_result)
self.assertEqual(
context.exception.partial_result.inserted_count, max_write_batch_size + 1
)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)
@async_client_context.require_version_min(8, 0, 0, -24)
async def test_collects_write_errors_across_batches_unordered(self):
listener = OvertCommandListener()
client = await async_rs_or_single_client(event_listeners=[listener])
self.addAsyncCleanup(client.aclose)
collection = client.db["coll"]
self.addAsyncCleanup(collection.drop)
await collection.drop()
await collection.insert_one(document={"_id": 1})
max_write_batch_size = (await async_client_context.hello)["maxWriteBatchSize"]
models = []
for _ in range(max_write_batch_size + 1):
models.append(
InsertOne(
namespace="db.coll",
document={"_id": 1},
)
)
with self.assertRaises(ClientBulkWriteException) as context:
await client.bulk_write(models=models, ordered=False)
self.assertEqual(len(context.exception.write_errors), max_write_batch_size + 1) # type: ignore[arg-type]
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)
@async_client_context.require_version_min(8, 0, 0, -24)
async def test_collects_write_errors_across_batches_ordered(self):
listener = OvertCommandListener()
client = await async_rs_or_single_client(event_listeners=[listener])
self.addAsyncCleanup(client.aclose)
collection = client.db["coll"]
self.addAsyncCleanup(collection.drop)
await collection.drop()
await collection.insert_one(document={"_id": 1})
max_write_batch_size = (await async_client_context.hello)["maxWriteBatchSize"]
models = []
for _ in range(max_write_batch_size + 1):
models.append(
InsertOne(
namespace="db.coll",
document={"_id": 1},
)
)
with self.assertRaises(ClientBulkWriteException) as context:
await client.bulk_write(models=models, ordered=True)
self.assertEqual(len(context.exception.write_errors), 1) # type: ignore[arg-type]
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 1)
@async_client_context.require_version_min(8, 0, 0, -24)
async def test_handles_cursor_requiring_getMore(self):
listener = OvertCommandListener()
client = await async_rs_or_single_client(event_listeners=[listener])
self.addAsyncCleanup(client.aclose)
collection = client.db["coll"]
self.addAsyncCleanup(collection.drop)
await collection.drop()
max_bson_object_size = (await async_client_context.hello)["maxBsonObjectSize"]
models = []
a_repeated = "a" * (max_bson_object_size // 2)
b_repeated = "b" * (max_bson_object_size // 2)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": a_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": b_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
result = await client.bulk_write(models=models, verbose_results=True)
self.assertEqual(result.upserted_count, 2)
self.assertEqual(len(result.update_results), 2)
get_more_event = False
for event in listener.started_events:
if event.command_name == "getMore":
get_more_event = True
self.assertTrue(get_more_event)
@async_client_context.require_version_min(8, 0, 0, -24)
@async_client_context.require_no_standalone
async def test_handles_cursor_requiring_getMore_within_transaction(self):
listener = OvertCommandListener()
client = await async_rs_or_single_client(event_listeners=[listener])
self.addAsyncCleanup(client.aclose)
collection = client.db["coll"]
self.addAsyncCleanup(collection.drop)
await collection.drop()
max_bson_object_size = (await async_client_context.hello)["maxBsonObjectSize"]
async with client.start_session() as session:
await session.start_transaction()
models = []
a_repeated = "a" * (max_bson_object_size // 2)
b_repeated = "b" * (max_bson_object_size // 2)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": a_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": b_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
result = await client.bulk_write(models=models, session=session, verbose_results=True)
self.assertEqual(result.upserted_count, 2)
self.assertEqual(len(result.update_results), 2)
get_more_event = False
for event in listener.started_events:
if event.command_name == "getMore":
get_more_event = True
self.assertTrue(get_more_event)
@async_client_context.require_version_min(8, 0, 0, -24)
@async_client_context.require_failCommand_fail_point
async def test_handles_getMore_error(self):
listener = OvertCommandListener()
client = await async_rs_or_single_client(event_listeners=[listener])
self.addAsyncCleanup(client.aclose)
collection = client.db["coll"]
self.addAsyncCleanup(collection.drop)
await collection.drop()
max_bson_object_size = (await async_client_context.hello)["maxBsonObjectSize"]
fail_command = {
"configureFailPoint": "failCommand",
"mode": {"times": 1},
"data": {"failCommands": ["getMore"], "errorCode": 8},
}
async with self.fail_point(fail_command):
models = []
a_repeated = "a" * (max_bson_object_size // 2)
b_repeated = "b" * (max_bson_object_size // 2)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": a_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": b_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
with self.assertRaises(ClientBulkWriteException) as context:
await client.bulk_write(models=models, verbose_results=True)
self.assertIsNotNone(context.exception.error)
self.assertEqual(context.exception.error["code"], 8)
self.assertIsNotNone(context.exception.partial_result)
self.assertEqual(context.exception.partial_result.upserted_count, 2)
self.assertEqual(len(context.exception.partial_result.update_results), 1)
get_more_event = False
kill_cursors_event = False
for event in listener.started_events:
if event.command_name == "getMore":
get_more_event = True
if event.command_name == "killCursors":
kill_cursors_event = True
self.assertTrue(get_more_event)
self.assertTrue(kill_cursors_event)
@async_client_context.require_version_min(8, 0, 0, -24)
async def test_returns_error_if_unacknowledged_too_large_insert(self):
listener = OvertCommandListener()
client = await async_rs_or_single_client(event_listeners=[listener])
self.addAsyncCleanup(client.aclose)
max_bson_object_size = (await async_client_context.hello)["maxBsonObjectSize"]
b_repeated = "b" * max_bson_object_size
# Insert document.
models_insert = [InsertOne(namespace="db.coll", document={"a": b_repeated})]
with self.assertRaises(DocumentTooLarge):
await client.bulk_write(models=models_insert, write_concern=WriteConcern(w=0))
# Replace document.
models_replace = [ReplaceOne(namespace="db.coll", filter={}, replacement={"a": b_repeated})]
with self.assertRaises(DocumentTooLarge):
await client.bulk_write(models=models_replace, write_concern=WriteConcern(w=0))
async def _setup_namespace_test_models(self):
max_message_size_bytes = (await async_client_context.hello)["maxMessageSizeBytes"]
max_bson_object_size = (await async_client_context.hello)["maxBsonObjectSize"]
ops_bytes = max_message_size_bytes - 1122
num_models = ops_bytes // max_bson_object_size
remainder_bytes = ops_bytes % max_bson_object_size
models = []
b_repeated = "b" * (max_bson_object_size - 57)
for _ in range(num_models):
models.append(
InsertOne(
namespace="db.coll",
document={"a": b_repeated},
)
)
if remainder_bytes >= 217:
num_models += 1
b_repeated = "b" * (remainder_bytes - 57)
models.append(
InsertOne(
namespace="db.coll",
document={"a": b_repeated},
)
)
return num_models, models
@async_client_context.require_version_min(8, 0, 0, -24)
async def test_no_batch_splits_if_new_namespace_is_not_too_large(self):
listener = OvertCommandListener()
client = await async_rs_or_single_client(event_listeners=[listener])
self.addAsyncCleanup(client.aclose)
num_models, models = await self._setup_namespace_test_models()
models.append(
InsertOne(
namespace="db.coll",
document={"a": "b"},
)
)
self.addAsyncCleanup(client.db["coll"].drop)
# No batch splitting required.
result = await client.bulk_write(models=models)
self.assertEqual(result.inserted_count, num_models + 1)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 1)
event = bulk_write_events[0]
self.assertEqual(len(event.command["ops"]), num_models + 1)
self.assertEqual(len(event.command["nsInfo"]), 1)
self.assertEqual(event.command["nsInfo"][0]["ns"], "db.coll")
@async_client_context.require_version_min(8, 0, 0, -24)
async def test_batch_splits_if_new_namespace_is_too_large(self):
listener = OvertCommandListener()
client = await async_rs_or_single_client(event_listeners=[listener])
self.addAsyncCleanup(client.aclose)
num_models, models = await self._setup_namespace_test_models()
c_repeated = "c" * 200
namespace = f"db.{c_repeated}"
models.append(
InsertOne(
namespace=namespace,
document={"a": "b"},
)
)
self.addAsyncCleanup(client.db["coll"].drop)
self.addAsyncCleanup(client.db[c_repeated].drop)
# Batch splitting required.
result = await client.bulk_write(models=models)
self.assertEqual(result.inserted_count, num_models + 1)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)
first_event, second_event = bulk_write_events
self.assertEqual(len(first_event.command["ops"]), num_models)
self.assertEqual(len(first_event.command["nsInfo"]), 1)
self.assertEqual(first_event.command["nsInfo"][0]["ns"], "db.coll")
self.assertEqual(len(second_event.command["ops"]), 1)
self.assertEqual(len(second_event.command["nsInfo"]), 1)
self.assertEqual(second_event.command["nsInfo"][0]["ns"], namespace)
@async_client_context.require_version_min(8, 0, 0, -24)
async def test_returns_error_if_no_writes_can_be_added_to_ops(self):
client = await async_rs_or_single_client()
self.addAsyncCleanup(client.aclose)
max_message_size_bytes = (await async_client_context.hello)["maxMessageSizeBytes"]
# Document too large.
b_repeated = "b" * max_message_size_bytes
models = [InsertOne(namespace="db.coll", document={"a": b_repeated})]
with self.assertRaises(InvalidOperation) as context:
await client.bulk_write(models=models)
self.assertIn("cannot do an empty bulk write", context.exception._message)
# Namespace too large.
c_repeated = "c" * max_message_size_bytes
namespace = f"db.{c_repeated}"
models = [InsertOne(namespace=namespace, document={"a": "b"})]
with self.assertRaises(InvalidOperation) as context:
await client.bulk_write(models=models)
self.assertIn("cannot do an empty bulk write", context.exception._message)
@async_client_context.require_version_min(8, 0, 0, -24)
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
async def test_returns_error_if_auto_encryption_configured(self):
opts = AutoEncryptionOpts(
key_vault_namespace="db.coll",
kms_providers={"aws": {"accessKeyId": "foo", "secretAccessKey": "bar"}},
)
client = await async_rs_or_single_client(auto_encryption_opts=opts)
self.addAsyncCleanup(client.aclose)
models = [InsertOne(namespace="db.coll", document={"a": "b"})]
with self.assertRaises(InvalidOperation) as context:
await client.bulk_write(models=models)
self.assertIn(
"bulk_write does not currently support automatic encryption", context.exception._message
)
# https://github.com/mongodb/specifications/blob/master/source/client-side-operations-timeout/tests/README.md#11-multi-batch-bulkwrites
class TestClientBulkWriteTimeout(AsyncIntegrationTest):
@async_client_context.require_version_min(8, 0, 0, -24)
@async_client_context.require_failCommand_fail_point
async def test_timeout_in_multi_batch_bulk_write(self):
internal_client = await async_rs_or_single_client(timeoutMS=None)
self.addAsyncCleanup(internal_client.aclose)
collection = internal_client.db["coll"]
self.addAsyncCleanup(collection.drop)
await collection.drop()
max_bson_object_size = (await async_client_context.hello)["maxBsonObjectSize"]
max_message_size_bytes = (await async_client_context.hello)["maxMessageSizeBytes"]
fail_command = {
"configureFailPoint": "failCommand",
"mode": {"times": 2},
"data": {"failCommands": ["bulkWrite"], "blockConnection": True, "blockTimeMS": 1010},
}
async with self.fail_point(fail_command):
models = []
num_models = int(max_message_size_bytes / max_bson_object_size + 1)
b_repeated = "b" * (max_bson_object_size - 500)
for _ in range(num_models):
models.append(
InsertOne(
namespace="db.coll",
document={"a": b_repeated},
)
)
listener = OvertCommandListener()
client = await async_rs_or_single_client(
event_listeners=[listener],
readConcernLevel="majority",
readPreference="primary",
timeoutMS=2000,
w="majority",
)
self.addAsyncCleanup(client.aclose)
with self.assertRaises(ClientBulkWriteException) as context:
await client.bulk_write(models=models)
self.assertIsInstance(context.exception.error, NetworkTimeout)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)

View File

@ -1380,41 +1380,39 @@ class TestCursor(AsyncIntegrationTest):
self.assertEqual("getMore", started[1].command_name)
self.assertNotIn("$readPreference", started[1].command)
@async_client_context.require_version_min(4, 0)
@async_client_context.require_replica_set
async def test_to_list_tailable(self):
oplog = self.client.local.oplog.rs
last = await oplog.find().sort("$natural", pymongo.DESCENDING).limit(-1).next()
ts = last["ts"]
# Set maxAwaitTimeMS=1 to speed up the test and avoid blocking on the noop writer.
c = oplog.find(
{"ts": {"$gte": ts}}, cursor_type=pymongo.CursorType.TAILABLE_AWAIT, oplog_replay=True
)
).max_await_time_ms(1)
self.addAsyncCleanup(c.close)
docs = await c.to_list()
self.assertGreaterEqual(len(docs), 1)
async def test_to_list_empty(self):
c = self.db.does_not_exist.find()
docs = await c.to_list()
self.assertEqual([], docs)
@async_client_context.require_replica_set
@async_client_context.require_change_streams
async def test_command_cursor_to_list(self):
c = await self.db.test.aggregate([{"$changeStream": {}}])
# Set maxAwaitTimeMS=1 to speed up the test.
c = await self.db.test.aggregate([{"$changeStream": {}}], maxAwaitTimeMS=1)
self.addAsyncCleanup(c.close)
docs = await c.to_list()
self.assertGreaterEqual(len(docs), 0)
@async_client_context.require_replica_set
@async_client_context.require_change_streams
async def test_command_cursor_to_list_empty(self):
c = await self.db.does_not_exist.aggregate([{"$changeStream": {}}])
# Set maxAwaitTimeMS=1 to speed up the test.
c = await self.db.does_not_exist.aggregate([{"$changeStream": {}}], maxAwaitTimeMS=1)
self.addAsyncCleanup(c.close)
docs = await c.to_list()
self.assertEqual([], docs)

View File

@ -116,10 +116,10 @@ class TestDatabaseNoConnect(unittest.TestCase):
with self.assertRaises(TypeError):
_ = db[0]
# next fails
with self.assertRaisesRegex(TypeError, "'Database' object is not iterable"):
with self.assertRaisesRegex(TypeError, "'AsyncDatabase' object is not iterable"):
_ = next(db)
# .next() fails
with self.assertRaisesRegex(TypeError, "'Database' object is not iterable"):
with self.assertRaisesRegex(TypeError, "'AsyncDatabase' object is not iterable"):
_ = db.next()
# Do not implement typing.Iterable.
self.assertNotIsInstance(db, Iterable)

View File

@ -5,6 +5,7 @@
{
"client": {
"id": "client",
"useMultipleMongoses": false,
"observeLogMessages": {
"command": "debug"
}

View File

@ -0,0 +1,218 @@
{
"description": "unacknowledged-client-bulkWrite",
"schemaVersion": "1.7",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"createEntities": [
{
"client": {
"id": "client",
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent",
"commandSucceededEvent",
"commandFailedEvent"
],
"uriOptions": {
"w": 0
}
}
},
{
"database": {
"id": "database",
"client": "client",
"databaseName": "command-monitoring-tests"
}
},
{
"collection": {
"id": "collection",
"database": "database",
"collectionName": "test"
}
}
],
"initialData": [
{
"collectionName": "test",
"databaseName": "command-monitoring-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
],
"_yamlAnchors": {
"namespace": "command-monitoring-tests.test"
},
"tests": [
{
"description": "A successful mixed client bulkWrite",
"operations": [
{
"object": "client",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "command-monitoring-tests.test",
"document": {
"_id": 4,
"x": 44
}
}
},
{
"updateOne": {
"namespace": "command-monitoring-tests.test",
"filter": {
"_id": 3
},
"update": {
"$set": {
"x": 333
}
}
}
}
]
},
"expectResult": {
"insertedCount": {
"$$unsetOrMatches": 0
},
"upsertedCount": {
"$$unsetOrMatches": 0
},
"matchedCount": {
"$$unsetOrMatches": 0
},
"modifiedCount": {
"$$unsetOrMatches": 0
},
"deletedCount": {
"$$unsetOrMatches": 0
},
"insertResults": {
"$$unsetOrMatches": {}
},
"updateResults": {
"$$unsetOrMatches": {}
},
"deleteResults": {
"$$unsetOrMatches": {}
}
}
},
{
"object": "collection",
"name": "find",
"arguments": {
"filter": {}
},
"expectResult": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 333
},
{
"_id": 4,
"x": 44
}
]
}
],
"expectEvents": [
{
"client": "client",
"ignoreExtraEvents": true,
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": true,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 4,
"x": 44
}
},
{
"update": 0,
"filter": {
"_id": 3
},
"updateMods": {
"$set": {
"x": 333
}
},
"multi": false
}
],
"nsInfo": [
{
"ns": "command-monitoring-tests.test"
}
]
}
}
},
{
"commandSucceededEvent": {
"commandName": "bulkWrite",
"reply": {
"ok": 1,
"nInserted": {
"$$exists": false
},
"nMatched": {
"$$exists": false
},
"nModified": {
"$$exists": false
},
"nUpserted": {
"$$exists": false
},
"nDeleted": {
"$$exists": false
}
}
}
}
]
}
]
}
]
}

View File

@ -5,6 +5,7 @@
{
"client": {
"id": "client",
"useMultipleMongoses": false,
"observeEvents": [
"commandStartedEvent",
"commandSucceededEvent",
@ -70,17 +71,7 @@
"object": "collection",
"arguments": {
"filter": {}
},
"expectResult": [
{
"_id": 1,
"x": 11
},
{
"_id": "unorderedBulkWriteInsertW0",
"x": 44
}
]
}
}
],
"expectEvents": [

View File

@ -152,4 +152,4 @@
]
}
]
}
}

View File

@ -0,0 +1,267 @@
{
"description": "client bulkWrite delete options",
"schemaVersion": "1.1",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "crud-tests"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
}
],
"initialData": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
],
"_yamlAnchors": {
"namespace": "crud-tests.coll0",
"collation": {
"locale": "simple"
},
"hint": "_id_"
},
"tests": [
{
"description": "client bulk write delete with collation",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
},
"collation": {
"locale": "simple"
}
}
},
{
"deleteMany": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": {
"$gt": 1
}
},
"collation": {
"locale": "simple"
}
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 0,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 3,
"insertResults": {},
"updateResults": {},
"deleteResults": {
"0": {
"deletedCount": 1
},
"1": {
"deletedCount": 2
}
}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"delete": 0,
"filter": {
"_id": 1
},
"collation": {
"locale": "simple"
},
"multi": false
},
{
"delete": 0,
"filter": {
"_id": {
"$gt": 1
}
},
"collation": {
"locale": "simple"
},
"multi": true
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"databaseName": "crud-tests",
"collectionName": "coll0",
"documents": []
}
]
},
{
"description": "client bulk write delete with hint",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
},
"hint": "_id_"
}
},
{
"deleteMany": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": {
"$gt": 1
}
},
"hint": "_id_"
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 0,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 3,
"insertResults": {},
"updateResults": {},
"deleteResults": {
"0": {
"deletedCount": 1
},
"1": {
"deletedCount": 2
}
}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"delete": 0,
"filter": {
"_id": 1
},
"hint": "_id_",
"multi": false
},
{
"delete": 0,
"filter": {
"_id": {
"$gt": 1
}
},
"hint": "_id_",
"multi": true
}
]
}
}
}
]
}
],
"outcome": [
{
"databaseName": "crud-tests",
"collectionName": "coll0",
"documents": []
}
]
}
]
}

View File

@ -0,0 +1,68 @@
{
"description": "client bulkWrite errorResponse",
"schemaVersion": "1.12",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"useMultipleMongoses": false
}
}
],
"_yamlAnchors": {
"namespace": "crud-tests.coll0"
},
"tests": [
{
"description": "client bulkWrite operations support errorResponse assertions",
"operations": [
{
"name": "failPoint",
"object": "testRunner",
"arguments": {
"client": "client0",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"bulkWrite"
],
"errorCode": 8
}
}
}
},
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 1
}
}
}
]
},
"expectError": {
"errorCode": 8,
"errorResponse": {
"code": 8
}
}
}
]
}
]
}

View File

@ -0,0 +1,454 @@
{
"description": "client bulkWrite errors",
"schemaVersion": "1.21",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
],
"uriOptions": {
"retryWrites": false
},
"useMultipleMongoses": false
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "crud-tests"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
}
],
"initialData": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
],
"_yamlAnchors": {
"namespace": "crud-tests.coll0",
"writeConcernErrorCode": 91,
"writeConcernErrorMessage": "Replication is being shut down",
"undefinedVarCode": 17276
},
"tests": [
{
"description": "an individual operation fails during an ordered bulkWrite",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
}
}
},
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"$expr": {
"$eq": [
"$_id",
"$$id2"
]
}
}
}
},
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 3
}
}
}
],
"verboseResults": true
},
"expectError": {
"expectResult": {
"insertedCount": 0,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 1,
"insertResults": {},
"updateResults": {},
"deleteResults": {
"0": {
"deletedCount": 1
}
}
},
"writeErrors": {
"1": {
"code": 17276
}
}
}
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
]
},
{
"description": "an individual operation fails during an unordered bulkWrite",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
}
}
},
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"$expr": {
"$eq": [
"$_id",
"$$id2"
]
}
}
}
},
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 3
}
}
}
],
"verboseResults": true,
"ordered": false
},
"expectError": {
"expectResult": {
"insertedCount": 0,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 2,
"insertResults": {},
"updateResults": {},
"deleteResults": {
"0": {
"deletedCount": 1
},
"2": {
"deletedCount": 1
}
}
},
"writeErrors": {
"1": {
"code": 17276
}
}
}
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 2,
"x": 22
}
]
}
]
},
{
"description": "detailed results are omitted from error when verboseResults is false",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
}
}
},
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"$expr": {
"$eq": [
"$_id",
"$$id2"
]
}
}
}
},
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 3
}
}
}
],
"verboseResults": false
},
"expectError": {
"expectResult": {
"insertedCount": 0,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 1,
"insertResults": {
"$$unsetOrMatches": {}
},
"updateResults": {
"$$unsetOrMatches": {}
},
"deleteResults": {
"$$unsetOrMatches": {}
}
},
"writeErrors": {
"1": {
"code": 17276
}
}
}
}
]
},
{
"description": "a top-level failure occurs during a bulkWrite",
"operations": [
{
"object": "testRunner",
"name": "failPoint",
"arguments": {
"client": "client0",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"bulkWrite"
],
"errorCode": 8
}
}
}
},
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"x": 1
}
}
}
],
"verboseResults": true
},
"expectError": {
"errorCode": 8
}
}
]
},
{
"description": "a bulk write with only errors does not report a partial result",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"$expr": {
"$eq": [
"$_id",
"$$id2"
]
}
}
}
}
],
"verboseResults": true
},
"expectError": {
"expectResult": {
"$$unsetOrMatches": {}
},
"writeErrors": {
"0": {
"code": 17276
}
}
}
}
]
},
{
"description": "a write concern error occurs during a bulkWrite",
"operations": [
{
"name": "failPoint",
"object": "testRunner",
"arguments": {
"client": "client0",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"bulkWrite"
],
"writeConcernError": {
"code": 91,
"errmsg": "Replication is being shut down"
}
}
}
}
},
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 10
}
}
}
],
"verboseResults": true
},
"expectError": {
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {
"0": {
"insertedId": 10
}
},
"updateResults": {},
"deleteResults": {}
},
"writeConcernErrors": [
{
"code": 91,
"message": "Replication is being shut down"
}
]
}
}
]
},
{
"description": "an empty list of write models is a client-side error",
"operations": [
{
"name": "clientBulkWrite",
"object": "client0",
"arguments": {
"models": [],
"verboseResults": true
},
"expectError": {
"isClientError": true
}
}
]
}
]
}

View File

@ -0,0 +1,314 @@
{
"description": "client bulkWrite with mixed namespaces",
"schemaVersion": "1.1",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "db0"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
},
{
"collection": {
"id": "collection1",
"database": "database0",
"collectionName": "coll1"
}
},
{
"database": {
"id": "database1",
"client": "client0",
"databaseName": "db1"
}
},
{
"collection": {
"id": "collection2",
"database": "database1",
"collectionName": "coll2"
}
}
],
"initialData": [
{
"databaseName": "db0",
"collectionName": "coll0",
"documents": []
},
{
"databaseName": "db0",
"collectionName": "coll1",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
]
},
{
"databaseName": "db1",
"collectionName": "coll2",
"documents": [
{
"_id": 3,
"x": 33
},
{
"_id": 4,
"x": 44
}
]
}
],
"_yamlAnchors": {
"db0Coll0Namespace": "db0.coll0",
"db0Coll1Namespace": "db0.coll1",
"db1Coll2Namespace": "db1.coll2"
},
"tests": [
{
"description": "client bulkWrite with mixed namespaces",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "db0.coll0",
"document": {
"_id": 1
}
}
},
{
"insertOne": {
"namespace": "db0.coll0",
"document": {
"_id": 2
}
}
},
{
"updateOne": {
"namespace": "db0.coll1",
"filter": {
"_id": 1
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"deleteOne": {
"namespace": "db1.coll2",
"filter": {
"_id": 3
}
}
},
{
"deleteOne": {
"namespace": "db0.coll1",
"filter": {
"_id": 2
}
}
},
{
"replaceOne": {
"namespace": "db1.coll2",
"filter": {
"_id": 4
},
"replacement": {
"x": 45
}
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 2,
"upsertedCount": 0,
"matchedCount": 2,
"modifiedCount": 2,
"deletedCount": 2,
"insertResults": {
"0": {
"insertedId": 1
},
"1": {
"insertedId": 2
}
},
"updateResults": {
"2": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
},
"5": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
}
},
"deleteResults": {
"3": {
"deletedCount": 1
},
"4": {
"deletedCount": 1
}
}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"bulkWrite": 1,
"ops": [
{
"insert": 0,
"document": {
"_id": 1
}
},
{
"insert": 0,
"document": {
"_id": 2
}
},
{
"update": 1,
"filter": {
"_id": 1
},
"updateMods": {
"$inc": {
"x": 1
}
},
"multi": false
},
{
"delete": 2,
"filter": {
"_id": 3
},
"multi": false
},
{
"delete": 1,
"filter": {
"_id": 2
},
"multi": false
},
{
"update": 2,
"filter": {
"_id": 4
},
"updateMods": {
"x": 45
},
"multi": false
}
],
"nsInfo": [
{
"ns": "db0.coll0"
},
{
"ns": "db0.coll1"
},
{
"ns": "db1.coll2"
}
]
}
}
}
]
}
],
"outcome": [
{
"databaseName": "db0",
"collectionName": "coll0",
"documents": [
{
"_id": 1
},
{
"_id": 2
}
]
},
{
"databaseName": "db0",
"collectionName": "coll1",
"documents": [
{
"_id": 1,
"x": 12
}
]
},
{
"databaseName": "db1",
"collectionName": "coll2",
"documents": [
{
"_id": 4,
"x": 45
}
]
}
]
}
]
}

View File

@ -0,0 +1,715 @@
{
"description": "client bulkWrite top-level options",
"schemaVersion": "1.1",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"client": {
"id": "writeConcernClient",
"uriOptions": {
"w": 1
},
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "crud-tests"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
}
],
"initialData": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
]
}
],
"_yamlAnchors": {
"namespace": "crud-tests.coll0",
"comment": {
"bulk": "write"
},
"let": {
"id1": 1,
"id2": 2
},
"writeConcern": {
"w": "majority"
}
},
"tests": [
{
"description": "client bulkWrite comment",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 3,
"x": 33
}
}
}
],
"comment": {
"bulk": "write"
},
"verboseResults": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {
"0": {
"insertedId": 3
}
},
"updateResults": {},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"comment": {
"bulk": "write"
},
"ops": [
{
"insert": 0,
"document": {
"_id": 3,
"x": 33
}
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
]
},
{
"description": "client bulkWrite bypassDocumentValidation",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 3,
"x": 33
}
}
}
],
"bypassDocumentValidation": true,
"verboseResults": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {
"0": {
"insertedId": 3
}
},
"updateResults": {},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"bypassDocumentValidation": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 3,
"x": 33
}
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
]
},
{
"description": "client bulkWrite let",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"updateOne": {
"namespace": "crud-tests.coll0",
"filter": {
"$expr": {
"$eq": [
"$_id",
"$$id1"
]
}
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"$expr": {
"$eq": [
"$_id",
"$$id2"
]
}
}
}
}
],
"let": {
"id1": 1,
"id2": 2
},
"verboseResults": true
},
"expectResult": {
"insertedCount": 0,
"upsertedCount": 0,
"matchedCount": 1,
"modifiedCount": 1,
"deletedCount": 1,
"insertResults": {},
"updateResults": {
"0": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
}
},
"deleteResults": {
"1": {
"deletedCount": 1
}
}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"let": {
"id1": 1,
"id2": 2
},
"ops": [
{
"update": 0,
"filter": {
"$expr": {
"$eq": [
"$_id",
"$$id1"
]
}
},
"updateMods": {
"$inc": {
"x": 1
}
},
"multi": false
},
{
"delete": 0,
"filter": {
"$expr": {
"$eq": [
"$_id",
"$$id2"
]
}
},
"multi": false
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"databaseName": "crud-tests",
"collectionName": "coll0",
"documents": [
{
"_id": 1,
"x": 12
}
]
}
]
},
{
"description": "client bulkWrite bypassDocumentValidation: false is sent",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 3,
"x": 33
}
}
}
],
"bypassDocumentValidation": false,
"verboseResults": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {
"0": {
"insertedId": 3
}
},
"updateResults": {},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"bypassDocumentValidation": false,
"ops": [
{
"insert": 0,
"document": {
"_id": 3,
"x": 33
}
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
]
},
{
"description": "client bulkWrite writeConcern",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 3,
"x": 33
}
}
}
],
"writeConcern": {
"w": "majority"
},
"verboseResults": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {
"0": {
"insertedId": 3
}
},
"updateResults": {},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"writeConcern": {
"w": "majority"
},
"ops": [
{
"insert": 0,
"document": {
"_id": 3,
"x": 33
}
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
]
},
{
"description": "client bulkWrite inherits writeConcern from client",
"operations": [
{
"object": "writeConcernClient",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 3,
"x": 33
}
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {
"0": {
"insertedId": 3
}
},
"updateResults": {},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "writeConcernClient",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"writeConcern": {
"w": 1
},
"ops": [
{
"insert": 0,
"document": {
"_id": 3,
"x": 33
}
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
]
},
{
"description": "client bulkWrite writeConcern option overrides client writeConcern",
"operations": [
{
"object": "writeConcernClient",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 3,
"x": 33
}
}
}
],
"writeConcern": {
"w": "majority"
},
"verboseResults": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {
"0": {
"insertedId": 3
}
},
"updateResults": {},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "writeConcernClient",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"writeConcern": {
"w": "majority"
},
"ops": [
{
"insert": 0,
"document": {
"_id": 3,
"x": 33
}
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,290 @@
{
"description": "client bulkWrite with ordered option",
"schemaVersion": "1.1",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "crud-tests"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
}
],
"initialData": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": []
}
],
"_yamlAnchors": {
"namespace": "crud-tests.coll0"
},
"tests": [
{
"description": "client bulkWrite with ordered: false",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 1,
"x": 11
}
}
}
],
"verboseResults": true,
"ordered": false
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {
"0": {
"insertedId": 1
}
},
"updateResults": {},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": false,
"ops": [
{
"insert": 0,
"document": {
"_id": 1,
"x": 11
}
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
}
]
}
]
},
{
"description": "client bulkWrite with ordered: true",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 1,
"x": 11
}
}
}
],
"verboseResults": true,
"ordered": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {
"0": {
"insertedId": 1
}
},
"updateResults": {},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 1,
"x": 11
}
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
}
]
}
]
},
{
"description": "client bulkWrite defaults to ordered: true",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 1,
"x": 11
}
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {
"0": {
"insertedId": 1
}
},
"updateResults": {},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 1,
"x": 11
}
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
}
]
}
]
}
]
}

View File

@ -0,0 +1,832 @@
{
"description": "client bulkWrite results",
"schemaVersion": "1.1",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "crud-tests"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
}
],
"initialData": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
},
{
"_id": 5,
"x": 55
},
{
"_id": 6,
"x": 66
},
{
"_id": 7,
"x": 77
}
]
}
],
"_yamlAnchors": {
"namespace": "crud-tests.coll0"
},
"tests": [
{
"description": "client bulkWrite with verboseResults: true returns detailed results",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 8,
"x": 88
}
}
},
{
"updateOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"updateMany": {
"namespace": "crud-tests.coll0",
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"update": {
"$inc": {
"x": 2
}
}
}
},
{
"replaceOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 4
},
"replacement": {
"x": 44
},
"upsert": true
}
},
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 5
}
}
},
{
"deleteMany": {
"namespace": "crud-tests.coll0",
"filter": {
"$and": [
{
"_id": {
"$gt": 5
}
},
{
"_id": {
"$lte": 7
}
}
]
}
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 1,
"matchedCount": 3,
"modifiedCount": 3,
"deletedCount": 3,
"insertResults": {
"0": {
"insertedId": 8
}
},
"updateResults": {
"1": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
},
"2": {
"matchedCount": 2,
"modifiedCount": 2,
"upsertedId": {
"$$exists": false
}
},
"3": {
"matchedCount": 1,
"modifiedCount": 0,
"upsertedId": 4
}
},
"deleteResults": {
"4": {
"deletedCount": 1
},
"5": {
"deletedCount": 2
}
}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 8,
"x": 88
}
},
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$inc": {
"x": 1
}
},
"multi": false
},
{
"update": 0,
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"updateMods": {
"$inc": {
"x": 2
}
},
"multi": true
},
{
"update": 0,
"filter": {
"_id": 4
},
"updateMods": {
"x": 44
},
"upsert": true,
"multi": false
},
{
"delete": 0,
"filter": {
"_id": 5
},
"multi": false
},
{
"delete": 0,
"filter": {
"$and": [
{
"_id": {
"$gt": 5
}
},
{
"_id": {
"$lte": 7
}
}
]
},
"multi": true
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 12
},
{
"_id": 2,
"x": 24
},
{
"_id": 3,
"x": 35
},
{
"_id": 4,
"x": 44
},
{
"_id": 8,
"x": 88
}
]
}
]
},
{
"description": "client bulkWrite with verboseResults: false omits detailed results",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 8,
"x": 88
}
}
},
{
"updateOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"updateMany": {
"namespace": "crud-tests.coll0",
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"update": {
"$inc": {
"x": 2
}
}
}
},
{
"replaceOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 4
},
"replacement": {
"x": 44
},
"upsert": true
}
},
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 5
}
}
},
{
"deleteMany": {
"namespace": "crud-tests.coll0",
"filter": {
"$and": [
{
"_id": {
"$gt": 5
}
},
{
"_id": {
"$lte": 7
}
}
]
}
}
}
],
"verboseResults": false
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 1,
"matchedCount": 3,
"modifiedCount": 3,
"deletedCount": 3,
"insertResults": {
"$$unsetOrMatches": {}
},
"updateResults": {
"$$unsetOrMatches": {}
},
"deleteResults": {
"$$unsetOrMatches": {}
}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": true,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 8,
"x": 88
}
},
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$inc": {
"x": 1
}
},
"multi": false
},
{
"update": 0,
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"updateMods": {
"$inc": {
"x": 2
}
},
"multi": true
},
{
"update": 0,
"filter": {
"_id": 4
},
"updateMods": {
"x": 44
},
"upsert": true,
"multi": false
},
{
"delete": 0,
"filter": {
"_id": 5
},
"multi": false
},
{
"delete": 0,
"filter": {
"$and": [
{
"_id": {
"$gt": 5
}
},
{
"_id": {
"$lte": 7
}
}
]
},
"multi": true
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 12
},
{
"_id": 2,
"x": 24
},
{
"_id": 3,
"x": 35
},
{
"_id": 4,
"x": 44
},
{
"_id": 8,
"x": 88
}
]
}
]
},
{
"description": "client bulkWrite defaults to verboseResults: false",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "crud-tests.coll0",
"document": {
"_id": 8,
"x": 88
}
}
},
{
"updateOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"updateMany": {
"namespace": "crud-tests.coll0",
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"update": {
"$inc": {
"x": 2
}
}
}
},
{
"replaceOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 4
},
"replacement": {
"x": 44
},
"upsert": true
}
},
{
"deleteOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 5
}
}
},
{
"deleteMany": {
"namespace": "crud-tests.coll0",
"filter": {
"$and": [
{
"_id": {
"$gt": 5
}
},
{
"_id": {
"$lte": 7
}
}
]
}
}
}
]
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 1,
"matchedCount": 3,
"modifiedCount": 3,
"deletedCount": 3,
"insertResults": {
"$$unsetOrMatches": {}
},
"updateResults": {
"$$unsetOrMatches": {}
},
"deleteResults": {
"$$unsetOrMatches": {}
}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": true,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 8,
"x": 88
}
},
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$inc": {
"x": 1
}
},
"multi": false
},
{
"update": 0,
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"updateMods": {
"$inc": {
"x": 2
}
},
"multi": true
},
{
"update": 0,
"filter": {
"_id": 4
},
"updateMods": {
"x": 44
},
"upsert": true,
"multi": false
},
{
"delete": 0,
"filter": {
"_id": 5
},
"multi": false
},
{
"delete": 0,
"filter": {
"$and": [
{
"_id": {
"$gt": 5
}
},
{
"_id": {
"$lte": 7
}
}
]
},
"multi": true
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 12
},
{
"_id": 2,
"x": 24
},
{
"_id": 3,
"x": 35
},
{
"_id": 4,
"x": 44
},
{
"_id": 8,
"x": 88
}
]
}
]
}
]
}

View File

@ -0,0 +1,948 @@
{
"description": "client bulkWrite update options",
"schemaVersion": "1.1",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "crud-tests"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
}
],
"initialData": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"array": [
1,
2,
3
]
},
{
"_id": 2,
"array": [
1,
2,
3
]
},
{
"_id": 3,
"array": [
1,
2,
3
]
},
{
"_id": 4,
"array": [
1,
2,
3
]
}
]
}
],
"_yamlAnchors": {
"namespace": "crud-tests.coll0",
"collation": {
"locale": "simple"
},
"hint": "_id_"
},
"tests": [
{
"description": "client bulkWrite update with arrayFilters",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"updateOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
},
"update": {
"$set": {
"array.$[i]": 4
}
},
"arrayFilters": [
{
"i": {
"$gte": 2
}
}
]
}
},
{
"updateMany": {
"namespace": "crud-tests.coll0",
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"update": {
"$set": {
"array.$[i]": 5
}
},
"arrayFilters": [
{
"i": {
"$gte": 2
}
}
]
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 0,
"upsertedCount": 0,
"matchedCount": 3,
"modifiedCount": 3,
"deletedCount": 0,
"insertResults": {},
"updateResults": {
"0": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
},
"1": {
"matchedCount": 2,
"modifiedCount": 2,
"upsertedId": {
"$$exists": false
}
}
},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$set": {
"array.$[i]": 4
}
},
"arrayFilters": [
{
"i": {
"$gte": 2
}
}
],
"multi": false
},
{
"update": 0,
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"updateMods": {
"$set": {
"array.$[i]": 5
}
},
"arrayFilters": [
{
"i": {
"$gte": 2
}
}
],
"multi": true
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"databaseName": "crud-tests",
"collectionName": "coll0",
"documents": [
{
"_id": 1,
"array": [
1,
4,
4
]
},
{
"_id": 2,
"array": [
1,
5,
5
]
},
{
"_id": 3,
"array": [
1,
5,
5
]
},
{
"_id": 4,
"array": [
1,
2,
3
]
}
]
}
]
},
{
"description": "client bulkWrite update with collation",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"updateOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
},
"update": {
"$set": {
"array": [
1,
2,
4
]
}
},
"collation": {
"locale": "simple"
}
}
},
{
"updateMany": {
"namespace": "crud-tests.coll0",
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"update": {
"$set": {
"array": [
1,
2,
5
]
}
},
"collation": {
"locale": "simple"
}
}
},
{
"replaceOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 4
},
"replacement": {
"array": [
1,
2,
6
]
},
"collation": {
"locale": "simple"
}
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 0,
"upsertedCount": 0,
"matchedCount": 4,
"modifiedCount": 4,
"deletedCount": 0,
"insertResults": {},
"updateResults": {
"0": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
},
"1": {
"matchedCount": 2,
"modifiedCount": 2,
"upsertedId": {
"$$exists": false
}
},
"2": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
}
},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$set": {
"array": [
1,
2,
4
]
}
},
"collation": {
"locale": "simple"
},
"multi": false
},
{
"update": 0,
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"updateMods": {
"$set": {
"array": [
1,
2,
5
]
}
},
"collation": {
"locale": "simple"
},
"multi": true
},
{
"update": 0,
"filter": {
"_id": 4
},
"updateMods": {
"array": [
1,
2,
6
]
},
"collation": {
"locale": "simple"
},
"multi": false
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"databaseName": "crud-tests",
"collectionName": "coll0",
"documents": [
{
"_id": 1,
"array": [
1,
2,
4
]
},
{
"_id": 2,
"array": [
1,
2,
5
]
},
{
"_id": 3,
"array": [
1,
2,
5
]
},
{
"_id": 4,
"array": [
1,
2,
6
]
}
]
}
]
},
{
"description": "client bulkWrite update with hint",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"updateOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
},
"update": {
"$set": {
"array": [
1,
2,
4
]
}
},
"hint": "_id_"
}
},
{
"updateMany": {
"namespace": "crud-tests.coll0",
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"update": {
"$set": {
"array": [
1,
2,
5
]
}
},
"hint": "_id_"
}
},
{
"replaceOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 4
},
"replacement": {
"array": [
1,
2,
6
]
},
"hint": "_id_"
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 0,
"upsertedCount": 0,
"matchedCount": 4,
"modifiedCount": 4,
"deletedCount": 0,
"insertResults": {},
"updateResults": {
"0": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
},
"1": {
"matchedCount": 2,
"modifiedCount": 2,
"upsertedId": {
"$$exists": false
}
},
"2": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
}
},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$set": {
"array": [
1,
2,
4
]
}
},
"hint": "_id_",
"multi": false
},
{
"update": 0,
"filter": {
"$and": [
{
"_id": {
"$gt": 1
}
},
{
"_id": {
"$lte": 3
}
}
]
},
"updateMods": {
"$set": {
"array": [
1,
2,
5
]
}
},
"hint": "_id_",
"multi": true
},
{
"update": 0,
"filter": {
"_id": 4
},
"updateMods": {
"array": [
1,
2,
6
]
},
"hint": "_id_",
"multi": false
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"databaseName": "crud-tests",
"collectionName": "coll0",
"documents": [
{
"_id": 1,
"array": [
1,
2,
4
]
},
{
"_id": 2,
"array": [
1,
2,
5
]
},
{
"_id": 3,
"array": [
1,
2,
5
]
},
{
"_id": 4,
"array": [
1,
2,
6
]
}
]
}
]
},
{
"description": "client bulkWrite update with upsert",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"updateOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 5
},
"update": {
"$set": {
"array": [
1,
2,
4
]
}
},
"upsert": true
}
},
{
"replaceOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 6
},
"replacement": {
"array": [
1,
2,
6
]
},
"upsert": true
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 0,
"upsertedCount": 2,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {},
"updateResults": {
"0": {
"matchedCount": 1,
"modifiedCount": 0,
"upsertedId": 5
},
"1": {
"matchedCount": 1,
"modifiedCount": 0,
"upsertedId": 6
}
},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"update": 0,
"filter": {
"_id": 5
},
"updateMods": {
"$set": {
"array": [
1,
2,
4
]
}
},
"upsert": true,
"multi": false
},
{
"update": 0,
"filter": {
"_id": 6
},
"updateMods": {
"array": [
1,
2,
6
]
},
"upsert": true,
"multi": false
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"databaseName": "crud-tests",
"collectionName": "coll0",
"documents": [
{
"_id": 1,
"array": [
1,
2,
3
]
},
{
"_id": 2,
"array": [
1,
2,
3
]
},
{
"_id": 3,
"array": [
1,
2,
3
]
},
{
"_id": 4,
"array": [
1,
2,
3
]
},
{
"_id": 5,
"array": [
1,
2,
4
]
},
{
"_id": 6,
"array": [
1,
2,
6
]
}
]
}
]
}
]
}

View File

@ -0,0 +1,257 @@
{
"description": "client bulkWrite update pipeline",
"schemaVersion": "1.1",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "crud-tests"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
}
],
"initialData": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 1
},
{
"_id": 2,
"x": 2
}
]
}
],
"_yamlAnchors": {
"namespace": "crud-tests.coll0"
},
"tests": [
{
"description": "client bulkWrite updateOne with pipeline",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"updateOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
},
"update": [
{
"$addFields": {
"foo": 1
}
}
]
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 0,
"upsertedCount": 0,
"matchedCount": 1,
"modifiedCount": 1,
"deletedCount": 0,
"insertResults": {},
"updateResults": {
"0": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
}
},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": [
{
"$addFields": {
"foo": 1
}
}
],
"multi": false
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"databaseName": "crud-tests",
"collectionName": "coll0",
"documents": [
{
"_id": 1,
"x": 1,
"foo": 1
},
{
"_id": 2,
"x": 2
}
]
}
]
},
{
"description": "client bulkWrite updateMany with pipeline",
"operations": [
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"updateMany": {
"namespace": "crud-tests.coll0",
"filter": {},
"update": [
{
"$addFields": {
"foo": 1
}
}
]
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 0,
"upsertedCount": 0,
"matchedCount": 2,
"modifiedCount": 2,
"deletedCount": 0,
"insertResults": {},
"updateResults": {
"0": {
"matchedCount": 2,
"modifiedCount": 2,
"upsertedId": {
"$$exists": false
}
}
},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"update": 0,
"filter": {},
"updateMods": [
{
"$addFields": {
"foo": 1
}
}
],
"multi": true
}
],
"nsInfo": [
{
"ns": "crud-tests.coll0"
}
]
}
}
}
]
}
],
"outcome": [
{
"databaseName": "crud-tests",
"collectionName": "coll0",
"documents": [
{
"_id": 1,
"x": 1,
"foo": 1
},
{
"_id": 2,
"x": 2,
"foo": 1
}
]
}
]
}
]
}

View File

@ -0,0 +1,216 @@
{
"description": "client-bulkWrite-update-validation",
"schemaVersion": "1.1",
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "crud-tests"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
}
],
"initialData": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
],
"_yamlAnchors": {
"namespace": "crud-tests.coll0"
},
"tests": [
{
"description": "client bulkWrite replaceOne prohibits atomic modifiers",
"operations": [
{
"name": "clientBulkWrite",
"object": "client0",
"arguments": {
"models": [
{
"replaceOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
},
"replacement": {
"$set": {
"x": 22
}
}
}
}
]
},
"expectError": {
"isClientError": true
}
}
],
"expectEvents": [
{
"client": "client0",
"events": []
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
]
},
{
"description": "client bulkWrite updateOne requires atomic modifiers",
"operations": [
{
"name": "clientBulkWrite",
"object": "client0",
"arguments": {
"models": [
{
"updateOne": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": 1
},
"update": {
"x": 22
}
}
}
]
},
"expectError": {
"isClientError": true
}
}
],
"expectEvents": [
{
"client": "client0",
"events": []
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
]
},
{
"description": "client bulkWrite updateMany requires atomic modifiers",
"operations": [
{
"name": "clientBulkWrite",
"object": "client0",
"arguments": {
"models": [
{
"updateMany": {
"namespace": "crud-tests.coll0",
"filter": {
"_id": {
"$gt": 1
}
},
"update": {
"x": 44
}
}
}
]
},
"expectError": {
"isClientError": true
}
}
],
"expectEvents": [
{
"client": "client0",
"events": []
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "crud-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
]
}
]
}

View File

@ -1,53 +0,0 @@
{
"collection_name": "driverdata",
"database_name": "test",
"tests": [
{
"description": "Aggregate with pipeline (project, sort, limit)",
"operations": [
{
"object": "collection",
"name": "aggregate",
"arguments": {
"pipeline": [
{
"$project": {
"_id": 0
}
},
{
"$sort": {
"a": 1
}
},
{
"$limit": 2
}
]
},
"result": [
{
"a": 1,
"b": 2,
"c": 3
},
{
"a": 2,
"b": 3,
"c": 4
}
]
}
],
"expectations": [
{
"command_started_event": {
"command": {
"aggregate": "driverdata"
}
}
}
]
}
]
}

View File

@ -1,27 +0,0 @@
{
"collection_name": "driverdata",
"database_name": "test",
"tests": [
{
"description": "estimatedDocumentCount succeeds",
"operations": [
{
"object": "collection",
"name": "estimatedDocumentCount",
"result": 15
}
],
"expectations": [
{
"command_started_event": {
"command": {
"count": "driverdata"
},
"command_name": "count",
"database_name": "test"
}
}
]
}
]
}

View File

@ -1,57 +0,0 @@
{
"collection_name": "driverdata",
"database_name": "test",
"tests": [
{
"description": "A successful find event with getMore",
"operations": [
{
"object": "collection",
"name": "find",
"arguments": {
"filter": {
"a": {
"$gte": 2
}
},
"sort": {
"a": 1
},
"batchSize": 3,
"limit": 4
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"find": "driverdata",
"filter": {
"a": {
"$gte": 2
}
},
"sort": {
"a": 1
},
"batchSize": 3,
"limit": 4
},
"command_name": "find",
"database_name": "test"
}
},
{
"command_started_event": {
"command": {
"batchSize": 1
},
"command_name": "getMore",
"database_name": "cursors"
}
}
]
}
]
}

View File

@ -1,25 +0,0 @@
{
"database_name": "test",
"tests": [
{
"description": "ListCollections succeeds",
"operations": [
{
"name": "listCollections",
"object": "database"
}
],
"expectations": [
{
"command_started_event": {
"command_name": "listCollections",
"database_name": "test",
"command": {
"listCollections": 1
}
}
}
]
}
]
}

View File

@ -1,24 +0,0 @@
{
"tests": [
{
"description": "ListDatabases succeeds",
"operations": [
{
"name": "listDatabases",
"object": "client"
}
],
"expectations": [
{
"command_started_event": {
"command_name": "listDatabases",
"database_name": "admin",
"command": {
"listDatabases": 1
}
}
}
]
}
]
}

View File

@ -1,31 +0,0 @@
{
"database_name": "test",
"tests": [
{
"description": "ping succeeds using runCommand",
"operations": [
{
"name": "runCommand",
"object": "database",
"command_name": "ping",
"arguments": {
"command": {
"ping": 1
}
}
}
],
"expectations": [
{
"command_started_event": {
"command_name": "ping",
"database_name": "test",
"command": {
"ping": 1
}
}
}
]
}
]
}

View File

@ -0,0 +1,84 @@
{
"description": "aggregate",
"schemaVersion": "1.0",
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "test"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "driverdata"
}
}
],
"tests": [
{
"description": "Aggregate with pipeline (project, sort, limit)",
"operations": [
{
"object": "collection0",
"name": "aggregate",
"arguments": {
"pipeline": [
{
"$project": {
"_id": 0
}
},
{
"$sort": {
"a": 1
}
},
{
"$limit": 2
}
]
},
"expectResult": [
{
"a": 1,
"b": 2,
"c": 3
},
{
"a": 2,
"b": 3,
"c": 4
}
]
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"aggregate": "driverdata"
},
"commandName": "aggregate",
"databaseName": "test"
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,56 @@
{
"description": "estimatedDocumentCount",
"schemaVersion": "1.0",
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "test"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "driverdata"
}
}
],
"tests": [
{
"description": "estimatedDocumentCount succeeds",
"operations": [
{
"object": "collection0",
"name": "estimatedDocumentCount",
"expectResult": 15
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"count": "driverdata"
},
"commandName": "count",
"databaseName": "test"
}
}
]
}
]
}
]
}

View File

@ -1,12 +1,36 @@
{
"collection_name": "driverdata",
"database_name": "test",
"description": "find",
"schemaVersion": "1.0",
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "test"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "driverdata"
}
}
],
"tests": [
{
"description": "Find with projection and sort",
"operations": [
{
"object": "collection",
"object": "collection0",
"name": "find",
"arguments": {
"filter": {
@ -22,7 +46,7 @@
},
"limit": 5
},
"result": [
"expectResult": [
{
"a": 5,
"b": 6,
@ -51,13 +75,20 @@
]
}
],
"expectations": [
"expectEvents": [
{
"command_started_event": {
"command": {
"find": "driverdata"
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"find": "driverdata"
},
"commandName": "find",
"databaseName": "test"
}
}
}
]
}
]
}

View File

@ -0,0 +1,95 @@
{
"description": "getMore",
"schemaVersion": "1.0",
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "test"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "driverdata"
}
}
],
"tests": [
{
"description": "A successful find event with getMore",
"operations": [
{
"object": "collection0",
"name": "find",
"arguments": {
"filter": {
"a": {
"$gte": 2
}
},
"sort": {
"a": 1
},
"batchSize": 3,
"limit": 4
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"find": "driverdata",
"filter": {
"a": {
"$gte": 2
}
},
"sort": {
"a": 1
},
"batchSize": 3,
"limit": 4
},
"commandName": "find",
"databaseName": "test"
}
},
{
"commandStartedEvent": {
"command": {
"getMore": {
"$$type": [
"int",
"long"
]
},
"collection": {
"$$type": "string"
},
"batchSize": 1
},
"commandName": "getMore",
"databaseName": "cursors"
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,48 @@
{
"description": "listCollections",
"schemaVersion": "1.0",
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "test"
}
}
],
"tests": [
{
"description": "ListCollections succeeds",
"operations": [
{
"object": "database0",
"name": "listCollections"
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"listCollections": 1
},
"commandName": "listCollections",
"databaseName": "test"
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,41 @@
{
"description": "listDatabases",
"schemaVersion": "1.0",
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
}
],
"tests": [
{
"description": "ListCollections succeeds",
"operations": [
{
"object": "client0",
"name": "listDatabases"
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"listDatabases": 1
},
"commandName": "listDatabases",
"databaseName": "admin"
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,54 @@
{
"description": "runCommand",
"schemaVersion": "1.0",
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "test"
}
}
],
"tests": [
{
"description": "ping succeeds using runCommand",
"operations": [
{
"object": "database0",
"name": "runCommand",
"arguments": {
"command": {
"ping": 1
},
"commandName": "ping"
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"ping": 1
},
"commandName": "ping",
"databaseName": "test"
}
}
]
}
]
}
]
}

View File

@ -16,7 +16,7 @@
"b:27017"
],
"minWireVersion": 0,
"maxWireVersion": 6
"maxWireVersion": 21
}
],
[

View File

@ -16,7 +16,7 @@
"b:27017"
],
"minWireVersion": 0,
"maxWireVersion": 6
"maxWireVersion": 21
}
]
],

View File

@ -23,7 +23,7 @@
"isWritablePrimary": true,
"msg": "isdbgrid",
"minWireVersion": 0,
"maxWireVersion": 6
"maxWireVersion": 21
}
]
],

View File

@ -11,7 +11,7 @@
"helloOk": true,
"isWritablePrimary": true,
"minWireVersion": 0,
"maxWireVersion": 6
"maxWireVersion": 21
}
]
],

View File

@ -1,5 +1,5 @@
{
"description": "Standalone with default maxWireVersion of 0 is upgraded to one with maxWireVersion 6",
"description": "Standalone with default maxWireVersion of 0 is upgraded to one with maxWireVersion 21",
"uri": "mongodb://a",
"phases": [
{
@ -35,7 +35,7 @@
"helloOk": true,
"isWritablePrimary": true,
"minWireVersion": 0,
"maxWireVersion": 6
"maxWireVersion": 21
}
]
],

View File

@ -4,11 +4,11 @@
"runOnRequirements": [
{
"minServerVersion": "4.9",
"serverless": "forbid",
"topologies": [
"replicaset",
"sharded"
],
"serverless": "forbid"
]
}
],
"createEntities": [
@ -39,13 +39,6 @@
"client": {
"id": "client",
"useMultipleMongoses": false,
"uriOptions": {
"connectTimeoutMS": 500,
"heartbeatFrequencyMS": 500,
"appname": "interruptInUse",
"retryReads": false,
"minPoolSize": 0
},
"observeEvents": [
"poolClearedEvent",
"connectionClosedEvent",
@ -54,7 +47,14 @@
"commandFailedEvent",
"connectionCheckedOutEvent",
"connectionCheckedInEvent"
]
],
"uriOptions": {
"connectTimeoutMS": 500,
"heartbeatFrequencyMS": 500,
"appname": "interruptInUse",
"retryReads": false,
"minPoolSize": 0
}
}
},
{
@ -83,7 +83,9 @@
"name": "insertOne",
"object": "collection",
"arguments": {
"document": { "_id" : 1 }
"document": {
"_id": 1
}
}
},
{
@ -92,14 +94,16 @@
"arguments": {
"thread": "thread1",
"operation": {
"name": "find",
"object": "collection",
"arguments": {
"filter": { "$where": "sleep(2000) || true" }
},
"expectError": {
"isError": true
"name": "find",
"object": "collection",
"arguments": {
"filter": {
"$where": "sleep(2000) || true"
}
},
"expectError": {
"isError": true
}
}
}
},
@ -107,6 +111,7 @@
"name": "failPoint",
"object": "testRunner",
"arguments": {
"client": "setupClient",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
@ -114,22 +119,22 @@
},
"data": {
"failCommands": [
"hello", "isMaster"
"hello",
"isMaster"
],
"blockConnection": true,
"blockTimeMS": 1500,
"appName": "interruptInUse"
}
},
"client": "setupClient"
}
}
},
{
"name": "waitForThread",
"object": "testRunner",
"arguments": {
"thread": "thread1"
}
"name": "waitForThread",
"object": "testRunner",
"arguments": {
"thread": "thread1"
}
}
],
"expectEvents": [
@ -157,20 +162,20 @@
"commandName": "find"
}
}
]
},
{
]
},
{
"client": "client",
"eventType": "cmap",
"events": [
{
"connectionCheckedOutEvent": { }
"connectionCheckedOutEvent": {}
},
{
"connectionCheckedInEvent": { }
"connectionCheckedInEvent": {}
},
{
"connectionCheckedOutEvent": { }
"connectionCheckedOutEvent": {}
},
{
"poolClearedEvent": {
@ -181,16 +186,22 @@
"connectionCheckedInEvent": {}
},
{
"connectionClosedEvent": { }
"connectionClosedEvent": {}
}
]
}
]
}
],
"outcome": [{
"collectionName": "interruptInUse",
"databaseName": "sdam-tests",
"documents": [{ "_id": 1 }]
}]
"outcome": [
{
"collectionName": "interruptInUse",
"databaseName": "sdam-tests",
"documents": [
{
"_id": 1
}
]
}
]
},
{
"description": "Error returned from connection pool clear with interruptInUseConnections=true is retryable",
@ -204,22 +215,22 @@
"client": {
"id": "client",
"useMultipleMongoses": false,
"observeEvents": [
"poolClearedEvent",
"connectionClosedEvent",
"commandStartedEvent",
"commandFailedEvent",
"commandSucceededEvent",
"connectionCheckedOutEvent",
"connectionCheckedInEvent"
],
"uriOptions": {
"connectTimeoutMS": 500,
"heartbeatFrequencyMS": 500,
"appname": "interruptInUseRetryable",
"retryReads": true,
"minPoolSize": 0
},
"observeEvents": [
"poolClearedEvent",
"connectionClosedEvent",
"commandFailedEvent",
"commandStartedEvent",
"commandSucceededEvent",
"connectionCheckedOutEvent",
"connectionCheckedInEvent"
]
}
}
},
{
@ -248,7 +259,9 @@
"name": "insertOne",
"object": "collection",
"arguments": {
"document": { "_id" : 1 }
"document": {
"_id": 1
}
}
},
{
@ -257,11 +270,13 @@
"arguments": {
"thread": "thread1",
"operation": {
"name": "find",
"object": "collection",
"arguments": {
"filter": { "$where": "sleep(2000) || true" }
"name": "find",
"object": "collection",
"arguments": {
"filter": {
"$where": "sleep(2000) || true"
}
}
}
}
},
@ -269,6 +284,7 @@
"name": "failPoint",
"object": "testRunner",
"arguments": {
"client": "setupClient",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
@ -276,22 +292,22 @@
},
"data": {
"failCommands": [
"hello", "isMaster"
"hello",
"isMaster"
],
"blockConnection": true,
"blockTimeMS": 1500,
"appName": "interruptInUseRetryable"
}
},
"client": "setupClient"
}
}
},
{
"name": "waitForThread",
"object": "testRunner",
"arguments": {
"thread": "thread1"
}
"name": "waitForThread",
"object": "testRunner",
"arguments": {
"thread": "thread1"
}
}
],
"expectEvents": [
@ -329,20 +345,20 @@
"commandName": "find"
}
}
]
},
{
]
},
{
"client": "client",
"eventType": "cmap",
"events": [
{
"connectionCheckedOutEvent": { }
"connectionCheckedOutEvent": {}
},
{
"connectionCheckedInEvent": { }
"connectionCheckedInEvent": {}
},
{
"connectionCheckedOutEvent": { }
"connectionCheckedOutEvent": {}
},
{
"poolClearedEvent": {
@ -353,7 +369,7 @@
"connectionCheckedInEvent": {}
},
{
"connectionClosedEvent": { }
"connectionClosedEvent": {}
},
{
"connectionCheckedOutEvent": {}
@ -361,14 +377,20 @@
{
"connectionCheckedInEvent": {}
}
]
}
]
}
],
"outcome": [{
"collectionName": "interruptInUse",
"databaseName": "sdam-tests",
"documents": [{ "_id": 1 }]
}]
"outcome": [
{
"collectionName": "interruptInUse",
"databaseName": "sdam-tests",
"documents": [
{
"_id": 1
}
]
}
]
},
{
"description": "Error returned from connection pool clear with interruptInUseConnections=true is retryable for write",
@ -382,22 +404,23 @@
"client": {
"id": "client",
"useMultipleMongoses": false,
"observeEvents": [
"poolClearedEvent",
"connectionClosedEvent",
"commandStartedEvent",
"commandFailedEvent",
"commandSucceededEvent",
"connectionCheckedOutEvent",
"connectionCheckedInEvent"
],
"uriOptions": {
"connectTimeoutMS": 500,
"heartbeatFrequencyMS": 500,
"appname": "interruptInUseRetryableWrite",
"retryWrites": true,
"minPoolSize": 0
},
"observeEvents": [
"poolClearedEvent",
"connectionClosedEvent",
"commandFailedEvent",
"commandStartedEvent",
"commandSucceededEvent",
"connectionCheckedOutEvent",
"connectionCheckedInEvent"
]}
}
}
},
{
"database": {
@ -425,7 +448,9 @@
"name": "insertOne",
"object": "collection",
"arguments": {
"document": { "_id": 1 }
"document": {
"_id": 1
}
}
},
{
@ -437,9 +462,15 @@
"name": "updateOne",
"object": "collection",
"arguments": {
"filter": { "$where": "sleep(2000) || true" },
"update": [ { "$set": { "a": "bar" } } ]
}
"filter": {
"$where": "sleep(2000) || true"
},
"update": {
"$set": {
"a": "bar"
}
}
}
}
}
},
@ -447,6 +478,7 @@
"name": "failPoint",
"object": "testRunner",
"arguments": {
"client": "setupClient",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
@ -454,22 +486,22 @@
},
"data": {
"failCommands": [
"hello", "isMaster"
"hello",
"isMaster"
],
"blockConnection": true,
"blockTimeMS": 1500,
"appName": "interruptInUseRetryableWrite"
}
},
"client": "setupClient"
}
}
},
{
"name": "waitForThread",
"object": "testRunner",
"arguments": {
"thread": "thread1"
}
"name": "waitForThread",
"object": "testRunner",
"arguments": {
"thread": "thread1"
}
}
],
"expectEvents": [
@ -507,20 +539,20 @@
"commandName": "update"
}
}
]
},
{
]
},
{
"client": "client",
"eventType": "cmap",
"events": [
{
"connectionCheckedOutEvent": { }
"connectionCheckedOutEvent": {}
},
{
"connectionCheckedInEvent": { }
"connectionCheckedInEvent": {}
},
{
"connectionCheckedOutEvent": { }
"connectionCheckedOutEvent": {}
},
{
"poolClearedEvent": {
@ -531,7 +563,7 @@
"connectionCheckedInEvent": {}
},
{
"connectionClosedEvent": { }
"connectionClosedEvent": {}
},
{
"connectionCheckedOutEvent": {}
@ -539,14 +571,21 @@
{
"connectionCheckedInEvent": {}
}
]
}
]
}
],
"outcome": [{
"collectionName": "interruptInUse",
"databaseName": "sdam-tests",
"documents": [{ "_id": 1, "a" : "bar"}]
}]
"outcome": [
{
"collectionName": "interruptInUse",
"databaseName": "sdam-tests",
"documents": [
{
"_id": 1,
"a": "bar"
}
]
}
]
}
]
}
}

View File

@ -0,0 +1,88 @@
{
"description": "loadbalanced-emit-topology-description-changed-before-close",
"schemaVersion": "1.20",
"runOnRequirements": [
{
"topologies": [
"load-balanced"
],
"minServerVersion": "4.4"
}
],
"tests": [
{
"description": "Topology lifecycle",
"operations": [
{
"name": "createEntities",
"object": "testRunner",
"arguments": {
"entities": [
{
"client": {
"id": "client",
"observeEvents": [
"topologyDescriptionChangedEvent",
"topologyOpeningEvent",
"topologyClosedEvent"
]
}
}
]
}
},
{
"name": "waitForEvent",
"object": "testRunner",
"arguments": {
"client": "client",
"event": {
"topologyDescriptionChangedEvent": {}
},
"count": 2
}
},
{
"name": "close",
"object": "client"
}
],
"expectEvents": [
{
"client": "client",
"eventType": "sdam",
"events": [
{
"topologyOpeningEvent": {}
},
{
"topologyDescriptionChangedEvent": {
"previousDescription": {
"type": "Unknown"
},
"newDescription": {}
}
},
{
"topologyDescriptionChangedEvent": {
"newDescription": {
"type": "LoadBalanced"
}
}
},
{
"topologyDescriptionChangedEvent": {
"newDescription": {
"type": "Unknown"
}
}
},
{
"topologyClosedEvent": {}
}
]
}
]
}
]
}

View File

@ -0,0 +1,89 @@
{
"description": "replicaset-emit-topology-description-changed-before-close",
"schemaVersion": "1.20",
"runOnRequirements": [
{
"topologies": [
"replicaset"
],
"minServerVersion": "4.4"
}
],
"tests": [
{
"description": "Topology lifecycle",
"operations": [
{
"name": "createEntities",
"object": "testRunner",
"arguments": {
"entities": [
{
"client": {
"id": "client",
"observeEvents": [
"topologyDescriptionChangedEvent",
"topologyOpeningEvent",
"topologyClosedEvent"
]
}
}
]
}
},
{
"name": "waitForEvent",
"object": "testRunner",
"arguments": {
"client": "client",
"event": {
"topologyDescriptionChangedEvent": {}
},
"count": 4
}
},
{
"name": "close",
"object": "client"
}
],
"expectEvents": [
{
"client": "client",
"eventType": "sdam",
"ignoreExtraEvents": false,
"events": [
{
"topologyOpeningEvent": {}
},
{
"topologyDescriptionChangedEvent": {}
},
{
"topologyDescriptionChangedEvent": {}
},
{
"topologyDescriptionChangedEvent": {}
},
{
"topologyDescriptionChangedEvent": {}
},
{
"topologyDescriptionChangedEvent": {
"previousDescription": {
"type": "ReplicaSetWithPrimary"
},
"newDescription": {
"type": "Unknown"
}
}
},
{
"topologyClosedEvent": {}
}
]
}
]
}
]
}

View File

@ -444,6 +444,69 @@
]
}
]
},
{
"description": "poll waits after successful heartbeat",
"operations": [
{
"name": "createEntities",
"object": "testRunner",
"arguments": {
"entities": [
{
"client": {
"id": "client",
"uriOptions": {
"serverMonitoringMode": "poll",
"heartbeatFrequencyMS": 1000000
},
"useMultipleMongoses": false,
"observeEvents": [
"serverHeartbeatStartedEvent",
"serverHeartbeatSucceededEvent"
]
}
},
{
"database": {
"id": "db",
"client": "client",
"databaseName": "sdam-tests"
}
}
]
}
},
{
"name": "waitForEvent",
"object": "testRunner",
"arguments": {
"client": "client",
"event": {
"serverHeartbeatSucceededEvent": {}
},
"count": 1
}
},
{
"name": "wait",
"object": "testRunner",
"arguments": {
"ms": 500
}
},
{
"name": "assertEventCount",
"object": "testRunner",
"arguments": {
"client": "client",
"event": {
"serverHeartbeatStartedEvent": {}
},
"count": 1
}
}
]
}
]
}

View File

@ -0,0 +1,108 @@
{
"description": "sharded-emit-topology-description-changed-before-close",
"schemaVersion": "1.20",
"runOnRequirements": [
{
"topologies": [
"sharded"
],
"minServerVersion": "4.4"
}
],
"tests": [
{
"description": "Topology lifecycle",
"operations": [
{
"name": "createEntities",
"object": "testRunner",
"arguments": {
"entities": [
{
"client": {
"id": "client",
"observeEvents": [
"topologyDescriptionChangedEvent",
"topologyOpeningEvent",
"topologyClosedEvent"
],
"useMultipleMongoses": true
}
}
]
}
},
{
"name": "waitForEvent",
"object": "testRunner",
"arguments": {
"client": "client",
"event": {
"topologyDescriptionChangedEvent": {}
},
"count": 3
}
},
{
"name": "close",
"object": "client"
}
],
"expectEvents": [
{
"client": "client",
"eventType": "sdam",
"ignoreExtraEvents": false,
"events": [
{
"topologyOpeningEvent": {}
},
{
"topologyDescriptionChangedEvent": {
"previousDescription": {
"type": "Unknown"
},
"newDescription": {
"type": "Unknown"
}
}
},
{
"topologyDescriptionChangedEvent": {
"previousDescription": {
"type": "Unknown"
},
"newDescription": {
"type": "Sharded"
}
}
},
{
"topologyDescriptionChangedEvent": {
"previousDescription": {
"type": "Sharded"
},
"newDescription": {
"type": "Sharded"
}
}
},
{
"topologyDescriptionChangedEvent": {
"previousDescription": {
"type": "Sharded"
},
"newDescription": {
"type": "Unknown"
}
}
},
{
"topologyClosedEvent": {}
}
]
}
]
}
]
}

View File

@ -0,0 +1,97 @@
{
"description": "standalone-emit-topology-description-changed-before-close",
"schemaVersion": "1.20",
"runOnRequirements": [
{
"topologies": [
"single"
],
"minServerVersion": "4.4"
}
],
"tests": [
{
"description": "Topology lifecycle",
"operations": [
{
"name": "createEntities",
"object": "testRunner",
"arguments": {
"entities": [
{
"client": {
"id": "client",
"observeEvents": [
"topologyDescriptionChangedEvent",
"topologyOpeningEvent",
"topologyClosedEvent"
]
}
}
]
}
},
{
"name": "waitForEvent",
"object": "testRunner",
"arguments": {
"client": "client",
"event": {
"topologyDescriptionChangedEvent": {}
},
"count": 2
}
},
{
"name": "close",
"object": "client"
}
],
"expectEvents": [
{
"client": "client",
"eventType": "sdam",
"ignoreExtraEvents": false,
"events": [
{
"topologyOpeningEvent": {}
},
{
"topologyDescriptionChangedEvent": {
"previousDescription": {
"type": "Unknown"
},
"newDescription": {
"type": "Unknown"
}
}
},
{
"topologyDescriptionChangedEvent": {
"previousDescription": {
"type": "Unknown"
},
"newDescription": {
"type": "Single"
}
}
},
{
"topologyDescriptionChangedEvent": {
"previousDescription": {
"type": "Single"
},
"newDescription": {
"type": "Unknown"
}
}
},
{
"topologyClosedEvent": {}
}
]
}
]
}
]
}

View File

@ -1,53 +1,93 @@
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
],
"collection_name": "default_write_concern_coll",
"database_name": "default_write_concern_db",
"runOn": [
"description": "default-write-concern-2.6",
"schemaVersion": "1.0",
"runOnRequirements": [
{
"minServerVersion": "2.6"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "default-write-concern-tests",
"databaseOptions": {
"writeConcern": {}
}
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll",
"collectionOptions": {
"writeConcern": {}
}
}
}
],
"initialData": [
{
"collectionName": "coll",
"databaseName": "default-write-concern-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
]
}
],
"tests": [
{
"description": "DeleteOne omits default write concern",
"operations": [
{
"name": "deleteOne",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"filter": {}
},
"result": {
"expectResult": {
"deletedCount": 1
}
}
],
"expectations": [
"expectEvents": [
{
"command_started_event": {
"command": {
"delete": "default_write_concern_coll",
"deletes": [
{
"q": {},
"limit": 1
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"delete": "coll",
"deletes": [
{
"q": {},
"limit": 1
}
],
"writeConcern": {
"$$exists": false
}
}
],
"writeConcern": null
}
}
}
]
}
]
},
@ -56,32 +96,36 @@
"operations": [
{
"name": "deleteMany",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"filter": {}
},
"result": {
"expectResult": {
"deletedCount": 2
}
}
],
"expectations": [
"expectEvents": [
{
"command_started_event": {
"command": {
"delete": "default_write_concern_coll",
"deletes": [
{
"q": {},
"limit": 0
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"delete": "coll",
"deletes": [
{
"q": {},
"limit": 0
}
],
"writeConcern": {
"$$exists": false
}
}
],
"writeConcern": null
}
}
}
]
}
]
},
@ -90,30 +134,24 @@
"operations": [
{
"name": "bulkWrite",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"ordered": true,
"requests": [
{
"name": "deleteMany",
"arguments": {
"deleteMany": {
"filter": {}
}
},
{
"name": "insertOne",
"arguments": {
"insertOne": {
"document": {
"_id": 1
}
}
},
{
"name": "updateOne",
"arguments": {
"updateOne": {
"filter": {
"_id": 1
},
@ -125,16 +163,14 @@
}
},
{
"name": "insertOne",
"arguments": {
"insertOne": {
"document": {
"_id": 2
}
}
},
{
"name": "replaceOne",
"arguments": {
"replaceOne": {
"filter": {
"_id": 1
},
@ -144,16 +180,14 @@
}
},
{
"name": "insertOne",
"arguments": {
"insertOne": {
"document": {
"_id": 3
}
}
},
{
"name": "updateMany",
"arguments": {
"updateMany": {
"filter": {
"_id": 1
},
@ -165,8 +199,7 @@
}
},
{
"name": "deleteOne",
"arguments": {
"deleteOne": {
"filter": {
"_id": 3
}
@ -176,10 +209,177 @@
}
}
],
"outcome": {
"collection": {
"name": "default_write_concern_coll",
"data": [
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"delete": "coll",
"deletes": [
{
"q": {},
"limit": 0
}
],
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"insert": "coll",
"documents": [
{
"_id": 1
}
],
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"update": "coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$set": {
"x": 1
}
},
"upsert": {
"$$unsetOrMatches": false
},
"multi": {
"$$unsetOrMatches": false
}
}
],
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"insert": "coll",
"documents": [
{
"_id": 2
}
],
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"update": "coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"x": 2
},
"upsert": {
"$$unsetOrMatches": false
},
"multi": {
"$$unsetOrMatches": false
}
}
],
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"insert": "coll",
"documents": [
{
"_id": 3
}
],
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"update": "coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$set": {
"x": 3
}
},
"multi": true,
"upsert": {
"$$unsetOrMatches": false
}
}
],
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"delete": "coll",
"deletes": [
{
"q": {
"_id": 3
},
"limit": 1
}
],
"writeConcern": {
"$$exists": false
}
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll",
"databaseName": "default-write-concern-tests",
"documents": [
{
"_id": 1,
"x": 3
@ -189,136 +389,6 @@
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"delete": "default_write_concern_coll",
"deletes": [
{
"q": {},
"limit": 0
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"insert": "default_write_concern_coll",
"documents": [
{
"_id": 1
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$set": {
"x": 1
}
}
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"insert": "default_write_concern_coll",
"documents": [
{
"_id": 2
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"x": 2
}
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"insert": "default_write_concern_coll",
"documents": [
{
"_id": 3
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$set": {
"x": 3
}
},
"multi": true
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"delete": "default_write_concern_coll",
"deletes": [
{
"q": {
"_id": 3
},
"limit": 1
}
],
"writeConcern": null
}
}
}
]
},
{
@ -326,10 +396,7 @@
"operations": [
{
"name": "insertOne",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"document": {
"_id": 3
@ -338,10 +405,7 @@
},
{
"name": "insertMany",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"documents": [
{
@ -354,10 +418,51 @@
}
}
],
"outcome": {
"collection": {
"name": "default_write_concern_coll",
"data": [
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"insert": "coll",
"documents": [
{
"_id": 3
}
],
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"insert": "coll",
"documents": [
{
"_id": 4
},
{
"_id": 5
}
],
"writeConcern": {
"$$exists": false
}
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll",
"databaseName": "default-write-concern-tests",
"documents": [
{
"_id": 1,
"x": 11
@ -377,37 +482,6 @@
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"insert": "default_write_concern_coll",
"documents": [
{
"_id": 3
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"insert": "default_write_concern_coll",
"documents": [
{
"_id": 4
},
{
"_id": 5
}
],
"writeConcern": null
}
}
}
]
},
{
@ -415,10 +489,7 @@
"operations": [
{
"name": "updateOne",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"filter": {
"_id": 1
@ -432,10 +503,7 @@
},
{
"name": "updateMany",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"filter": {
"_id": 2
@ -449,10 +517,7 @@
},
{
"name": "replaceOne",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"filter": {
"_id": 2
@ -463,10 +528,98 @@
}
}
],
"outcome": {
"collection": {
"name": "default_write_concern_coll",
"data": [
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"update": "coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$set": {
"x": 1
}
},
"upsert": {
"$$unsetOrMatches": false
},
"multi": {
"$$unsetOrMatches": false
}
}
],
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"update": "coll",
"updates": [
{
"q": {
"_id": 2
},
"u": {
"$set": {
"x": 2
}
},
"multi": true,
"upsert": {
"$$unsetOrMatches": false
}
}
],
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"update": "coll",
"updates": [
{
"q": {
"_id": 2
},
"u": {
"x": 3
},
"upsert": {
"$$unsetOrMatches": false
},
"multi": {
"$$unsetOrMatches": false
}
}
],
"writeConcern": {
"$$exists": false
}
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll",
"databaseName": "default-write-concern-tests",
"documents": [
{
"_id": 1,
"x": 1
@ -477,67 +630,6 @@
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$set": {
"x": 1
}
}
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 2
},
"u": {
"$set": {
"x": 2
}
},
"multi": true
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 2
},
"u": {
"x": 3
}
}
],
"writeConcern": null
}
}
}
]
}
]

View File

@ -1,31 +1,64 @@
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
],
"collection_name": "default_write_concern_coll",
"database_name": "default_write_concern_db",
"runOn": [
"description": "default-write-concern-3.2",
"schemaVersion": "1.0",
"runOnRequirements": [
{
"minServerVersion": "3.2"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "default-write-concern-tests",
"databaseOptions": {
"writeConcern": {}
}
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll",
"collectionOptions": {
"writeConcern": {}
}
}
}
],
"initialData": [
{
"collectionName": "coll",
"databaseName": "default-write-concern-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
]
}
],
"tests": [
{
"description": "findAndModify operations omit default write concern",
"operations": [
{
"name": "findOneAndUpdate",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"filter": {
"_id": 1
@ -39,10 +72,7 @@
},
{
"name": "findOneAndReplace",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"filter": {
"_id": 2
@ -54,10 +84,7 @@
},
{
"name": "findOneAndDelete",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"filter": {
"_id": 2
@ -65,60 +92,72 @@
}
}
],
"outcome": {
"collection": {
"name": "default_write_concern_coll",
"data": [
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"findAndModify": "coll",
"query": {
"_id": 1
},
"update": {
"$set": {
"x": 1
}
},
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"findAndModify": "coll",
"query": {
"_id": 2
},
"update": {
"x": 2
},
"writeConcern": {
"$$exists": false
}
}
}
},
{
"commandStartedEvent": {
"command": {
"findAndModify": "coll",
"query": {
"_id": 2
},
"remove": true,
"writeConcern": {
"$$exists": false
}
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll",
"databaseName": "default-write-concern-tests",
"documents": [
{
"_id": 1,
"x": 1
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"findAndModify": "default_write_concern_coll",
"query": {
"_id": 1
},
"update": {
"$set": {
"x": 1
}
},
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"findAndModify": "default_write_concern_coll",
"query": {
"_id": 2
},
"update": {
"x": 2
},
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"findAndModify": "default_write_concern_coll",
"query": {
"_id": 2
},
"remove": true,
"writeConcern": null
}
}
}
]
}
]

View File

@ -1,30 +1,68 @@
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
],
"collection_name": "default_write_concern_coll",
"database_name": "default_write_concern_db",
"runOn": [
"description": "default-write-concern-3.4",
"schemaVersion": "1.4",
"runOnRequirements": [
{
"minServerVersion": "3.4"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "default-write-concern-tests",
"databaseOptions": {
"writeConcern": {}
}
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll",
"collectionOptions": {
"writeConcern": {}
}
}
}
],
"initialData": [
{
"collectionName": "coll",
"databaseName": "default-write-concern-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
]
}
],
"tests": [
{
"description": "Aggregate with $out omits default write concern",
"runOnRequirements": [
{
"serverless": "forbid"
}
],
"operations": [
{
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"name": "aggregate",
"arguments": {
"pipeline": [
@ -42,77 +80,89 @@
}
}
],
"outcome": {
"collection": {
"name": "other_collection_name",
"data": [
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"aggregate": "coll",
"pipeline": [
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_collection_name"
}
],
"writeConcern": {
"$$exists": false
}
}
}
}
]
}
],
"outcome": [
{
"collectionName": "other_collection_name",
"databaseName": "default-write-concern-tests",
"documents": [
{
"_id": 2,
"x": 22
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"aggregate": "default_write_concern_coll",
"pipeline": [
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_collection_name"
}
],
"writeConcern": null
}
}
}
]
},
{
"description": "RunCommand with a write command omits default write concern (runCommand should never inherit write concern)",
"operations": [
{
"object": "database",
"databaseOptions": {
"writeConcern": {}
},
"object": "database0",
"name": "runCommand",
"command_name": "delete",
"arguments": {
"command": {
"delete": "default_write_concern_coll",
"delete": "coll",
"deletes": [
{
"q": {},
"limit": 1
}
]
}
},
"commandName": "delete"
}
}
],
"expectations": [
"expectEvents": [
{
"command_started_event": {
"command": {
"delete": "default_write_concern_coll",
"deletes": [
{
"q": {},
"limit": 1
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"delete": "coll",
"deletes": [
{
"q": {},
"limit": 1
}
],
"writeConcern": {
"$$exists": false
}
}
],
"writeConcern": null
}
}
}
]
}
]
},
@ -120,10 +170,7 @@
"description": "CreateIndex and dropIndex omits default write concern",
"operations": [
{
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"name": "createIndex",
"arguments": {
"keys": {
@ -132,53 +179,61 @@
}
},
{
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"name": "dropIndex",
"arguments": {
"name": "x_1"
}
}
],
"expectations": [
"expectEvents": [
{
"command_started_event": {
"command": {
"createIndexes": "default_write_concern_coll",
"indexes": [
{
"name": "x_1",
"key": {
"x": 1
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"createIndexes": "coll",
"indexes": [
{
"name": "x_1",
"key": {
"x": 1
}
}
],
"writeConcern": {
"$$exists": false
}
}
],
"writeConcern": null
}
},
{
"commandStartedEvent": {
"command": {
"dropIndexes": "coll",
"index": "x_1",
"writeConcern": {
"$$exists": false
}
}
}
}
}
},
{
"command_started_event": {
"command": {
"dropIndexes": "default_write_concern_coll",
"index": "x_1",
"writeConcern": null
}
}
]
}
]
},
{
"description": "MapReduce omits default write concern",
"runOnRequirements": [
{
"serverless": "forbid"
}
],
"operations": [
{
"name": "mapReduce",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"arguments": {
"map": {
"$code": "function inc() { return emit(0, this.x + 1) }"
@ -192,23 +247,30 @@
}
}
],
"expectations": [
"expectEvents": [
{
"command_started_event": {
"command": {
"mapReduce": "default_write_concern_coll",
"map": {
"$code": "function inc() { return emit(0, this.x + 1) }"
},
"reduce": {
"$code": "function sum(key, values) { return values.reduce((acc, x) => acc + x); }"
},
"out": {
"inline": 1
},
"writeConcern": null
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"mapReduce": "coll",
"map": {
"$code": "function inc() { return emit(0, this.x + 1) }"
},
"reduce": {
"$code": "function sum(key, values) { return values.reduce((acc, x) => acc + x); }"
},
"out": {
"inline": 1
},
"writeConcern": {
"$$exists": false
}
}
}
}
}
]
}
]
}

View File

@ -1,33 +1,63 @@
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
],
"collection_name": "default_write_concern_coll",
"database_name": "default_write_concern_db",
"runOn": [
"description": "default-write-concern-4.2",
"schemaVersion": "1.0",
"runOnRequirements": [
{
"minServerVersion": "4.2"
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
]
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "default-write-concern-tests",
"databaseOptions": {
"writeConcern": {}
}
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll",
"collectionOptions": {
"writeConcern": {}
}
}
}
],
"initialData": [
{
"collectionName": "coll",
"databaseName": "default-write-concern-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
]
}
],
"tests": [
{
"description": "Aggregate with $merge omits default write concern",
"operations": [
{
"object": "collection",
"databaseOptions": {
"writeConcern": {}
},
"collectionOptions": {
"writeConcern": {}
},
"object": "collection0",
"name": "aggregate",
"arguments": {
"pipeline": [
@ -47,41 +77,49 @@
}
}
],
"expectations": [
"expectEvents": [
{
"command_started_event": {
"command": {
"aggregate": "default_write_concern_coll",
"pipeline": [
{
"$match": {
"_id": {
"$gt": 1
"client": "client0",
"events": [
{
"commandStartedEvent": {
"command": {
"aggregate": "coll",
"pipeline": [
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$merge": {
"into": "other_collection_name"
}
}
}
},
{
"$merge": {
"into": "other_collection_name"
],
"writeConcern": {
"$$exists": false
}
}
],
"writeConcern": null
}
}
}
]
}
],
"outcome": {
"collection": {
"name": "other_collection_name",
"data": [
"outcome": [
{
"collectionName": "other_collection_name",
"databaseName": "default-write-concern-tests",
"documents": [
{
"_id": 2,
"x": 22
}
]
}
}
]
}
]
}

View File

@ -0,0 +1,350 @@
{
"description": "client bulkWrite retryable writes with client errors",
"schemaVersion": "1.21",
"runOnRequirements": [
{
"minServerVersion": "8.0",
"topologies": [
"replicaset",
"sharded",
"load-balanced"
]
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
],
"useMultipleMongoses": false
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "retryable-writes-tests"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
}
],
"initialData": [
{
"collectionName": "coll0",
"databaseName": "retryable-writes-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
],
"_yamlAnchors": {
"namespace": "retryable-writes-tests.coll0"
},
"tests": [
{
"description": "client bulkWrite with one network error succeeds after retry",
"operations": [
{
"object": "testRunner",
"name": "failPoint",
"arguments": {
"client": "client0",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"bulkWrite"
],
"closeConnection": true
}
}
}
},
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "retryable-writes-tests.coll0",
"document": {
"_id": 4,
"x": 44
}
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 0,
"modifiedCount": 0,
"deletedCount": 0,
"insertResults": {
"0": {
"insertedId": 4
}
},
"updateResults": {},
"deleteResults": {}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 4,
"x": 44
}
}
],
"nsInfo": [
{
"ns": "retryable-writes-tests.coll0"
}
],
"lsid": {
"$$exists": true
},
"txnNumber": {
"$$exists": true
}
}
}
},
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 4,
"x": 44
}
}
],
"nsInfo": [
{
"ns": "retryable-writes-tests.coll0"
}
],
"lsid": {
"$$exists": true
},
"txnNumber": {
"$$exists": true
}
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "retryable-writes-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
},
{
"_id": 4,
"x": 44
}
]
}
]
},
{
"description": "client bulkWrite with two network errors fails after retry",
"operations": [
{
"object": "testRunner",
"name": "failPoint",
"arguments": {
"client": "client0",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 2
},
"data": {
"failCommands": [
"bulkWrite"
],
"closeConnection": true
}
}
}
},
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "retryable-writes-tests.coll0",
"document": {
"_id": 4,
"x": 44
}
}
}
],
"verboseResults": true
},
"expectError": {
"isClientError": true,
"errorLabelsContain": [
"RetryableWriteError"
]
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 4,
"x": 44
}
}
],
"nsInfo": [
{
"ns": "retryable-writes-tests.coll0"
}
],
"lsid": {
"$$exists": true
},
"txnNumber": {
"$$exists": true
}
}
}
},
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 4,
"x": 44
}
}
],
"nsInfo": [
{
"ns": "retryable-writes-tests.coll0"
}
],
"lsid": {
"$$exists": true
},
"txnNumber": {
"$$exists": true
}
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "retryable-writes-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
]
}
]
}

View File

@ -0,0 +1,872 @@
{
"description": "client bulkWrite retryable writes",
"schemaVersion": "1.21",
"runOnRequirements": [
{
"minServerVersion": "8.0",
"topologies": [
"replicaset",
"sharded",
"load-balanced"
]
}
],
"createEntities": [
{
"client": {
"id": "client0",
"observeEvents": [
"commandStartedEvent"
],
"useMultipleMongoses": false
}
},
{
"client": {
"id": "clientRetryWritesFalse",
"uriOptions": {
"retryWrites": false
},
"observeEvents": [
"commandStartedEvent"
],
"useMultipleMongoses": false
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "retryable-writes-tests"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "coll0"
}
}
],
"initialData": [
{
"collectionName": "coll0",
"databaseName": "retryable-writes-tests",
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
],
"_yamlAnchors": {
"namespace": "retryable-writes-tests.coll0"
},
"tests": [
{
"description": "client bulkWrite with no multi: true operations succeeds after retryable top-level error",
"operations": [
{
"object": "testRunner",
"name": "failPoint",
"arguments": {
"client": "client0",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"bulkWrite"
],
"errorCode": 189,
"errorLabels": [
"RetryableWriteError"
]
}
}
}
},
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "retryable-writes-tests.coll0",
"document": {
"_id": 4,
"x": 44
}
}
},
{
"updateOne": {
"namespace": "retryable-writes-tests.coll0",
"filter": {
"_id": 1
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"replaceOne": {
"namespace": "retryable-writes-tests.coll0",
"filter": {
"_id": 2
},
"replacement": {
"x": 222
}
}
},
{
"deleteOne": {
"namespace": "retryable-writes-tests.coll0",
"filter": {
"_id": 3
}
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 2,
"modifiedCount": 2,
"deletedCount": 1,
"insertResults": {
"0": {
"insertedId": 4
}
},
"updateResults": {
"1": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
},
"2": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
}
},
"deleteResults": {
"3": {
"deletedCount": 1
}
}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 4,
"x": 44
}
},
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$inc": {
"x": 1
}
},
"multi": false
},
{
"update": 0,
"filter": {
"_id": 2
},
"updateMods": {
"x": 222
},
"multi": false
},
{
"delete": 0,
"filter": {
"_id": 3
},
"multi": false
}
],
"nsInfo": [
{
"ns": "retryable-writes-tests.coll0"
}
],
"lsid": {
"$$exists": true
},
"txnNumber": {
"$$exists": true
}
}
}
},
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 4,
"x": 44
}
},
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$inc": {
"x": 1
}
},
"multi": false
},
{
"update": 0,
"filter": {
"_id": 2
},
"updateMods": {
"x": 222
},
"multi": false
},
{
"delete": 0,
"filter": {
"_id": 3
},
"multi": false
}
],
"nsInfo": [
{
"ns": "retryable-writes-tests.coll0"
}
],
"lsid": {
"$$exists": true
},
"txnNumber": {
"$$exists": true
}
}
}
}
]
}
],
"outcome": [
{
"collectionName": "coll0",
"databaseName": "retryable-writes-tests",
"documents": [
{
"_id": 1,
"x": 12
},
{
"_id": 2,
"x": 222
},
{
"_id": 4,
"x": 44
}
]
}
]
},
{
"description": "client bulkWrite with multi: true operations fails after retryable top-level error",
"operations": [
{
"object": "testRunner",
"name": "failPoint",
"arguments": {
"client": "client0",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"bulkWrite"
],
"errorCode": 189,
"errorLabels": [
"RetryableWriteError"
]
}
}
}
},
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"updateMany": {
"namespace": "retryable-writes-tests.coll0",
"filter": {
"_id": 1
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"deleteMany": {
"namespace": "retryable-writes-tests.coll0",
"filter": {
"_id": 3
}
}
}
]
},
"expectError": {
"errorCode": 189,
"errorLabelsContain": [
"RetryableWriteError"
]
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": true,
"ordered": true,
"ops": [
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$inc": {
"x": 1
}
},
"multi": true
},
{
"delete": 0,
"filter": {
"_id": 3
},
"multi": true
}
],
"nsInfo": [
{
"ns": "retryable-writes-tests.coll0"
}
]
}
}
}
]
}
]
},
{
"description": "client bulkWrite with no multi: true operations succeeds after retryable writeConcernError",
"operations": [
{
"object": "testRunner",
"name": "failPoint",
"arguments": {
"client": "client0",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"bulkWrite"
],
"errorLabels": [
"RetryableWriteError"
],
"writeConcernError": {
"code": 91,
"errmsg": "Replication is being shut down"
}
}
}
}
},
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "retryable-writes-tests.coll0",
"document": {
"_id": 4,
"x": 44
}
}
},
{
"updateOne": {
"namespace": "retryable-writes-tests.coll0",
"filter": {
"_id": 1
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"replaceOne": {
"namespace": "retryable-writes-tests.coll0",
"filter": {
"_id": 2
},
"replacement": {
"x": 222
}
}
},
{
"deleteOne": {
"namespace": "retryable-writes-tests.coll0",
"filter": {
"_id": 3
}
}
}
],
"verboseResults": true
},
"expectResult": {
"insertedCount": 1,
"upsertedCount": 0,
"matchedCount": 2,
"modifiedCount": 2,
"deletedCount": 1,
"insertResults": {
"0": {
"insertedId": 4
}
},
"updateResults": {
"1": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
},
"2": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedId": {
"$$exists": false
}
}
},
"deleteResults": {
"3": {
"deletedCount": 1
}
}
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 4,
"x": 44
}
},
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$inc": {
"x": 1
}
},
"multi": false
},
{
"update": 0,
"filter": {
"_id": 2
},
"updateMods": {
"x": 222
},
"multi": false
},
{
"delete": 0,
"filter": {
"_id": 3
},
"multi": false
}
],
"nsInfo": [
{
"ns": "retryable-writes-tests.coll0"
}
],
"lsid": {
"$$exists": true
},
"txnNumber": {
"$$exists": true
}
}
}
},
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": false,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 4,
"x": 44
}
},
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$inc": {
"x": 1
}
},
"multi": false
},
{
"update": 0,
"filter": {
"_id": 2
},
"updateMods": {
"x": 222
},
"multi": false
},
{
"delete": 0,
"filter": {
"_id": 3
},
"multi": false
}
],
"nsInfo": [
{
"ns": "retryable-writes-tests.coll0"
}
],
"lsid": {
"$$exists": true
},
"txnNumber": {
"$$exists": true
}
}
}
}
]
}
]
},
{
"description": "client bulkWrite with multi: true operations fails after retryable writeConcernError",
"operations": [
{
"object": "testRunner",
"name": "failPoint",
"arguments": {
"client": "client0",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"bulkWrite"
],
"errorLabels": [
"RetryableWriteError"
],
"writeConcernError": {
"code": 91,
"errmsg": "Replication is being shut down"
}
}
}
}
},
{
"object": "client0",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"updateMany": {
"namespace": "retryable-writes-tests.coll0",
"filter": {
"_id": 1
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"deleteMany": {
"namespace": "retryable-writes-tests.coll0",
"filter": {
"_id": 3
}
}
}
]
},
"expectError": {
"writeConcernErrors": [
{
"code": 91,
"message": "Replication is being shut down"
}
]
}
}
],
"expectEvents": [
{
"client": "client0",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": true,
"ordered": true,
"ops": [
{
"update": 0,
"filter": {
"_id": 1
},
"updateMods": {
"$inc": {
"x": 1
}
},
"multi": true
},
{
"delete": 0,
"filter": {
"_id": 3
},
"multi": true
}
],
"nsInfo": [
{
"ns": "retryable-writes-tests.coll0"
}
]
}
}
}
]
}
]
},
{
"description": "client bulkWrite with retryWrites: false does not retry",
"operations": [
{
"object": "testRunner",
"name": "failPoint",
"arguments": {
"client": "clientRetryWritesFalse",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 1
},
"data": {
"failCommands": [
"bulkWrite"
],
"errorCode": 189,
"errorLabels": [
"RetryableWriteError"
]
}
}
}
},
{
"object": "clientRetryWritesFalse",
"name": "clientBulkWrite",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "retryable-writes-tests.coll0",
"document": {
"_id": 4,
"x": 44
}
}
}
]
},
"expectError": {
"errorCode": 189,
"errorLabelsContain": [
"RetryableWriteError"
]
}
}
],
"expectEvents": [
{
"client": "clientRetryWritesFalse",
"events": [
{
"commandStartedEvent": {
"commandName": "bulkWrite",
"databaseName": "admin",
"command": {
"bulkWrite": 1,
"errorsOnly": true,
"ordered": true,
"ops": [
{
"insert": 0,
"document": {
"_id": 4,
"x": 44
}
}
],
"nsInfo": [
{
"ns": "retryable-writes-tests.coll0"
}
]
}
}
}
]
}
]
}
]
}

View File

@ -53,6 +53,222 @@
}
],
"tests": [
{
"description": "client.clientBulkWrite succeeds after retryable handshake network error",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"operations": [
{
"name": "failPoint",
"object": "testRunner",
"arguments": {
"client": "client",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 2
},
"data": {
"failCommands": [
"ping",
"saslContinue"
],
"closeConnection": true
}
}
}
},
{
"name": "runCommand",
"object": "database",
"arguments": {
"commandName": "ping",
"command": {
"ping": 1
}
},
"expectError": {
"isError": true
}
},
{
"name": "clientBulkWrite",
"object": "client",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "retryable-writes-handshake-tests.coll",
"document": {
"_id": 8,
"x": 88
}
}
}
]
}
}
],
"expectEvents": [
{
"client": "client",
"eventType": "cmap",
"events": [
{
"connectionCheckOutStartedEvent": {}
},
{
"connectionCheckOutStartedEvent": {}
},
{
"connectionCheckOutStartedEvent": {}
},
{
"connectionCheckOutStartedEvent": {}
}
]
},
{
"client": "client",
"events": [
{
"commandStartedEvent": {
"command": {
"ping": 1
},
"databaseName": "retryable-writes-handshake-tests"
}
},
{
"commandFailedEvent": {
"commandName": "ping"
}
},
{
"commandStartedEvent": {
"commandName": "bulkWrite"
}
},
{
"commandSucceededEvent": {
"commandName": "bulkWrite"
}
}
]
}
]
},
{
"description": "client.clientBulkWrite succeeds after retryable handshake server error (ShutdownInProgress)",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"operations": [
{
"name": "failPoint",
"object": "testRunner",
"arguments": {
"client": "client",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": {
"times": 2
},
"data": {
"failCommands": [
"ping",
"saslContinue"
],
"closeConnection": true
}
}
}
},
{
"name": "runCommand",
"object": "database",
"arguments": {
"commandName": "ping",
"command": {
"ping": 1
}
},
"expectError": {
"isError": true
}
},
{
"name": "clientBulkWrite",
"object": "client",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "retryable-writes-handshake-tests.coll",
"document": {
"_id": 8,
"x": 88
}
}
}
]
}
}
],
"expectEvents": [
{
"client": "client",
"eventType": "cmap",
"events": [
{
"connectionCheckOutStartedEvent": {}
},
{
"connectionCheckOutStartedEvent": {}
},
{
"connectionCheckOutStartedEvent": {}
},
{
"connectionCheckOutStartedEvent": {}
}
]
},
{
"client": "client",
"events": [
{
"commandStartedEvent": {
"command": {
"ping": 1
},
"databaseName": "retryable-writes-handshake-tests"
}
},
{
"commandFailedEvent": {
"commandName": "ping"
}
},
{
"commandStartedEvent": {
"commandName": "bulkWrite"
}
},
{
"commandSucceededEvent": {
"commandName": "bulkWrite"
}
}
]
}
]
},
{
"description": "collection.insertOne succeeds after retryable handshake network error",
"operations": [

View File

@ -47,6 +47,9 @@
}
}
],
"_yamlAnchors": {
"namespace": "logging-tests.server-selection"
},
"tests": [
{
"description": "Successful bulkWrite operation: log messages have operationIds",
@ -224,6 +227,190 @@
]
}
]
},
{
"description": "Successful client bulkWrite operation: log messages have operationIds",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"operations": [
{
"name": "waitForEvent",
"object": "testRunner",
"arguments": {
"client": "client",
"event": {
"topologyDescriptionChangedEvent": {}
},
"count": 2
}
},
{
"name": "clientBulkWrite",
"object": "client",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "logging-tests.server-selection",
"document": {
"x": 1
}
}
}
]
}
}
],
"expectLogMessages": [
{
"client": "client",
"messages": [
{
"level": "debug",
"component": "serverSelection",
"data": {
"message": "Server selection started",
"operationId": {
"$$type": [
"int",
"long"
]
},
"operation": "bulkWrite"
}
},
{
"level": "debug",
"component": "serverSelection",
"data": {
"message": "Server selection succeeded",
"operationId": {
"$$type": [
"int",
"long"
]
},
"operation": "bulkWrite"
}
}
]
}
]
},
{
"description": "Failed client bulkWrite operation: log messages have operationIds",
"runOnRequirements": [
{
"minServerVersion": "8.0"
}
],
"operations": [
{
"name": "failPoint",
"object": "testRunner",
"arguments": {
"client": "failPointClient",
"failPoint": {
"configureFailPoint": "failCommand",
"mode": "alwaysOn",
"data": {
"failCommands": [
"hello",
"ismaster"
],
"appName": "loggingClient",
"closeConnection": true
}
}
}
},
{
"name": "waitForEvent",
"object": "testRunner",
"arguments": {
"client": "client",
"event": {
"serverDescriptionChangedEvent": {
"newDescription": {
"type": "Unknown"
}
}
},
"count": 1
}
},
{
"name": "clientBulkWrite",
"object": "client",
"arguments": {
"models": [
{
"insertOne": {
"namespace": "logging-tests.server-selection",
"document": {
"x": 1
}
}
}
]
},
"expectError": {
"isClientError": true
}
}
],
"expectLogMessages": [
{
"client": "client",
"messages": [
{
"level": "debug",
"component": "serverSelection",
"data": {
"message": "Server selection started",
"operationId": {
"$$type": [
"int",
"long"
]
},
"operation": "bulkWrite"
}
},
{
"level": "debug",
"component": "serverSelection",
"data": {
"message": "Waiting for suitable server to become available",
"operationId": {
"$$type": [
"int",
"long"
]
},
"operation": "bulkWrite"
}
},
{
"level": "debug",
"component": "serverSelection",
"data": {
"message": "Server selection failed",
"operationId": {
"$$type": [
"int",
"long"
]
},
"operation": "bulkWrite"
}
}
]
}
]
}
]
}

View File

@ -1020,21 +1020,32 @@ class TestCollectionChangeStream(TestChangeStreamBase, APITestsMixin, ProseSpecT
self.assertEqual(change["ns"]["coll"], self.watched_collection().name)
self.assertEqual(change["fullDocument"], raw_doc)
@client_context.require_version_min(4, 0) # Needed for start_at_operation_time.
def test_uuid_representations(self):
"""Test with uuid document _ids and different uuid_representation."""
optime = self.db.command("ping")["operationTime"]
self.watched_collection().insert_many(
[
{"_id": Binary(uuid.uuid4().bytes, id_subtype)}
for id_subtype in (STANDARD, PYTHON_LEGACY)
]
)
for uuid_representation in ALL_UUID_REPRESENTATIONS:
for id_subtype in (STANDARD, PYTHON_LEGACY):
options = self.watched_collection().codec_options.with_options(
uuid_representation=uuid_representation
)
coll = self.watched_collection(codec_options=options)
with coll.watch() as change_stream:
coll.insert_one({"_id": Binary(uuid.uuid4().bytes, id_subtype)})
_ = change_stream.next()
resume_token = change_stream.resume_token
options = self.watched_collection().codec_options.with_options(
uuid_representation=uuid_representation
)
coll = self.watched_collection(codec_options=options)
with coll.watch(start_at_operation_time=optime, max_await_time_ms=1) as change_stream:
_ = change_stream.next()
resume_token_1 = change_stream.resume_token
_ = change_stream.next()
resume_token_2 = change_stream.resume_token
# Should not error.
coll.watch(resume_after=resume_token)
# Should not error.
with coll.watch(resume_after=resume_token_1):
pass
with coll.watch(resume_after=resume_token_2):
pass
def test_document_id_order(self):
"""Test with document _ids that need their order preserved."""
@ -1053,7 +1064,8 @@ class TestCollectionChangeStream(TestChangeStreamBase, APITestsMixin, ProseSpecT
# The resume token is always a document.
self.assertIsInstance(resume_token, document_class)
# Should not error.
coll.watch(resume_after=resume_token)
with coll.watch(resume_after=resume_token):
pass
coll.delete_many({})
def test_read_concern(self):

View File

@ -0,0 +1,571 @@
# Copyright 2024-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Test the client bulk write API."""
from __future__ import annotations
import sys
sys.path[0:0] = [""]
from test import IntegrationTest, client_context, unittest
from test.utils import (
OvertCommandListener,
rs_or_single_client,
)
from pymongo.encryption_options import _HAVE_PYMONGOCRYPT, AutoEncryptionOpts
from pymongo.errors import (
ClientBulkWriteException,
DocumentTooLarge,
InvalidOperation,
NetworkTimeout,
)
from pymongo.monitoring import *
from pymongo.operations import *
from pymongo.write_concern import WriteConcern
_IS_SYNC = True
class TestClientBulkWrite(IntegrationTest):
@client_context.require_version_min(8, 0, 0, -24)
def test_returns_error_if_no_namespace_provided(self):
client = rs_or_single_client()
self.addCleanup(client.close)
models = [InsertOne(document={"a": "b"})]
with self.assertRaises(InvalidOperation) as context:
client.bulk_write(models=models)
self.assertIn(
"MongoClient.bulk_write requires a namespace to be provided for each write operation",
context.exception._message,
)
# https://github.com/mongodb/specifications/tree/master/source/crud/tests
class TestClientBulkWriteCRUD(IntegrationTest):
@client_context.require_version_min(8, 0, 0, -24)
def test_batch_splits_if_num_operations_too_large(self):
listener = OvertCommandListener()
client = rs_or_single_client(event_listeners=[listener])
self.addCleanup(client.close)
max_write_batch_size = (client_context.hello)["maxWriteBatchSize"]
models = []
for _ in range(max_write_batch_size + 1):
models.append(InsertOne(namespace="db.coll", document={"a": "b"}))
self.addCleanup(client.db["coll"].drop)
result = client.bulk_write(models=models)
self.assertEqual(result.inserted_count, max_write_batch_size + 1)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)
first_event, second_event = bulk_write_events
self.assertEqual(len(first_event.command["ops"]), max_write_batch_size)
self.assertEqual(len(second_event.command["ops"]), 1)
self.assertEqual(first_event.operation_id, second_event.operation_id)
@client_context.require_version_min(8, 0, 0, -24)
def test_batch_splits_if_ops_payload_too_large(self):
listener = OvertCommandListener()
client = rs_or_single_client(event_listeners=[listener])
self.addCleanup(client.close)
max_message_size_bytes = (client_context.hello)["maxMessageSizeBytes"]
max_bson_object_size = (client_context.hello)["maxBsonObjectSize"]
models = []
num_models = int(max_message_size_bytes / max_bson_object_size + 1)
b_repeated = "b" * (max_bson_object_size - 500)
for _ in range(num_models):
models.append(
InsertOne(
namespace="db.coll",
document={"a": b_repeated},
)
)
self.addCleanup(client.db["coll"].drop)
result = client.bulk_write(models=models)
self.assertEqual(result.inserted_count, num_models)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)
first_event, second_event = bulk_write_events
self.assertEqual(len(first_event.command["ops"]), num_models - 1)
self.assertEqual(len(second_event.command["ops"]), 1)
self.assertEqual(first_event.operation_id, second_event.operation_id)
@client_context.require_version_min(8, 0, 0, -24)
@client_context.require_failCommand_fail_point
def test_collects_write_concern_errors_across_batches(self):
listener = OvertCommandListener()
client = rs_or_single_client(
event_listeners=[listener],
retryWrites=False,
)
self.addCleanup(client.close)
max_write_batch_size = (client_context.hello)["maxWriteBatchSize"]
fail_command = {
"configureFailPoint": "failCommand",
"mode": {"times": 2},
"data": {
"failCommands": ["bulkWrite"],
"writeConcernError": {"code": 91, "errmsg": "Replication is being shut down"},
},
}
with self.fail_point(fail_command):
models = []
for _ in range(max_write_batch_size + 1):
models.append(
InsertOne(
namespace="db.coll",
document={"a": "b"},
)
)
self.addCleanup(client.db["coll"].drop)
with self.assertRaises(ClientBulkWriteException) as context:
client.bulk_write(models=models)
self.assertEqual(len(context.exception.write_concern_errors), 2) # type: ignore[arg-type]
self.assertIsNotNone(context.exception.partial_result)
self.assertEqual(
context.exception.partial_result.inserted_count, max_write_batch_size + 1
)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)
@client_context.require_version_min(8, 0, 0, -24)
def test_collects_write_errors_across_batches_unordered(self):
listener = OvertCommandListener()
client = rs_or_single_client(event_listeners=[listener])
self.addCleanup(client.close)
collection = client.db["coll"]
self.addCleanup(collection.drop)
collection.drop()
collection.insert_one(document={"_id": 1})
max_write_batch_size = (client_context.hello)["maxWriteBatchSize"]
models = []
for _ in range(max_write_batch_size + 1):
models.append(
InsertOne(
namespace="db.coll",
document={"_id": 1},
)
)
with self.assertRaises(ClientBulkWriteException) as context:
client.bulk_write(models=models, ordered=False)
self.assertEqual(len(context.exception.write_errors), max_write_batch_size + 1) # type: ignore[arg-type]
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)
@client_context.require_version_min(8, 0, 0, -24)
def test_collects_write_errors_across_batches_ordered(self):
listener = OvertCommandListener()
client = rs_or_single_client(event_listeners=[listener])
self.addCleanup(client.close)
collection = client.db["coll"]
self.addCleanup(collection.drop)
collection.drop()
collection.insert_one(document={"_id": 1})
max_write_batch_size = (client_context.hello)["maxWriteBatchSize"]
models = []
for _ in range(max_write_batch_size + 1):
models.append(
InsertOne(
namespace="db.coll",
document={"_id": 1},
)
)
with self.assertRaises(ClientBulkWriteException) as context:
client.bulk_write(models=models, ordered=True)
self.assertEqual(len(context.exception.write_errors), 1) # type: ignore[arg-type]
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 1)
@client_context.require_version_min(8, 0, 0, -24)
def test_handles_cursor_requiring_getMore(self):
listener = OvertCommandListener()
client = rs_or_single_client(event_listeners=[listener])
self.addCleanup(client.close)
collection = client.db["coll"]
self.addCleanup(collection.drop)
collection.drop()
max_bson_object_size = (client_context.hello)["maxBsonObjectSize"]
models = []
a_repeated = "a" * (max_bson_object_size // 2)
b_repeated = "b" * (max_bson_object_size // 2)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": a_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": b_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
result = client.bulk_write(models=models, verbose_results=True)
self.assertEqual(result.upserted_count, 2)
self.assertEqual(len(result.update_results), 2)
get_more_event = False
for event in listener.started_events:
if event.command_name == "getMore":
get_more_event = True
self.assertTrue(get_more_event)
@client_context.require_version_min(8, 0, 0, -24)
@client_context.require_no_standalone
def test_handles_cursor_requiring_getMore_within_transaction(self):
listener = OvertCommandListener()
client = rs_or_single_client(event_listeners=[listener])
self.addCleanup(client.close)
collection = client.db["coll"]
self.addCleanup(collection.drop)
collection.drop()
max_bson_object_size = (client_context.hello)["maxBsonObjectSize"]
with client.start_session() as session:
session.start_transaction()
models = []
a_repeated = "a" * (max_bson_object_size // 2)
b_repeated = "b" * (max_bson_object_size // 2)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": a_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": b_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
result = client.bulk_write(models=models, session=session, verbose_results=True)
self.assertEqual(result.upserted_count, 2)
self.assertEqual(len(result.update_results), 2)
get_more_event = False
for event in listener.started_events:
if event.command_name == "getMore":
get_more_event = True
self.assertTrue(get_more_event)
@client_context.require_version_min(8, 0, 0, -24)
@client_context.require_failCommand_fail_point
def test_handles_getMore_error(self):
listener = OvertCommandListener()
client = rs_or_single_client(event_listeners=[listener])
self.addCleanup(client.close)
collection = client.db["coll"]
self.addCleanup(collection.drop)
collection.drop()
max_bson_object_size = (client_context.hello)["maxBsonObjectSize"]
fail_command = {
"configureFailPoint": "failCommand",
"mode": {"times": 1},
"data": {"failCommands": ["getMore"], "errorCode": 8},
}
with self.fail_point(fail_command):
models = []
a_repeated = "a" * (max_bson_object_size // 2)
b_repeated = "b" * (max_bson_object_size // 2)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": a_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
models.append(
UpdateOne(
namespace="db.coll",
filter={"_id": b_repeated},
update={"$set": {"x": 1}},
upsert=True,
)
)
with self.assertRaises(ClientBulkWriteException) as context:
client.bulk_write(models=models, verbose_results=True)
self.assertIsNotNone(context.exception.error)
self.assertEqual(context.exception.error["code"], 8)
self.assertIsNotNone(context.exception.partial_result)
self.assertEqual(context.exception.partial_result.upserted_count, 2)
self.assertEqual(len(context.exception.partial_result.update_results), 1)
get_more_event = False
kill_cursors_event = False
for event in listener.started_events:
if event.command_name == "getMore":
get_more_event = True
if event.command_name == "killCursors":
kill_cursors_event = True
self.assertTrue(get_more_event)
self.assertTrue(kill_cursors_event)
@client_context.require_version_min(8, 0, 0, -24)
def test_returns_error_if_unacknowledged_too_large_insert(self):
listener = OvertCommandListener()
client = rs_or_single_client(event_listeners=[listener])
self.addCleanup(client.close)
max_bson_object_size = (client_context.hello)["maxBsonObjectSize"]
b_repeated = "b" * max_bson_object_size
# Insert document.
models_insert = [InsertOne(namespace="db.coll", document={"a": b_repeated})]
with self.assertRaises(DocumentTooLarge):
client.bulk_write(models=models_insert, write_concern=WriteConcern(w=0))
# Replace document.
models_replace = [ReplaceOne(namespace="db.coll", filter={}, replacement={"a": b_repeated})]
with self.assertRaises(DocumentTooLarge):
client.bulk_write(models=models_replace, write_concern=WriteConcern(w=0))
def _setup_namespace_test_models(self):
max_message_size_bytes = (client_context.hello)["maxMessageSizeBytes"]
max_bson_object_size = (client_context.hello)["maxBsonObjectSize"]
ops_bytes = max_message_size_bytes - 1122
num_models = ops_bytes // max_bson_object_size
remainder_bytes = ops_bytes % max_bson_object_size
models = []
b_repeated = "b" * (max_bson_object_size - 57)
for _ in range(num_models):
models.append(
InsertOne(
namespace="db.coll",
document={"a": b_repeated},
)
)
if remainder_bytes >= 217:
num_models += 1
b_repeated = "b" * (remainder_bytes - 57)
models.append(
InsertOne(
namespace="db.coll",
document={"a": b_repeated},
)
)
return num_models, models
@client_context.require_version_min(8, 0, 0, -24)
def test_no_batch_splits_if_new_namespace_is_not_too_large(self):
listener = OvertCommandListener()
client = rs_or_single_client(event_listeners=[listener])
self.addCleanup(client.close)
num_models, models = self._setup_namespace_test_models()
models.append(
InsertOne(
namespace="db.coll",
document={"a": "b"},
)
)
self.addCleanup(client.db["coll"].drop)
# No batch splitting required.
result = client.bulk_write(models=models)
self.assertEqual(result.inserted_count, num_models + 1)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 1)
event = bulk_write_events[0]
self.assertEqual(len(event.command["ops"]), num_models + 1)
self.assertEqual(len(event.command["nsInfo"]), 1)
self.assertEqual(event.command["nsInfo"][0]["ns"], "db.coll")
@client_context.require_version_min(8, 0, 0, -24)
def test_batch_splits_if_new_namespace_is_too_large(self):
listener = OvertCommandListener()
client = rs_or_single_client(event_listeners=[listener])
self.addCleanup(client.close)
num_models, models = self._setup_namespace_test_models()
c_repeated = "c" * 200
namespace = f"db.{c_repeated}"
models.append(
InsertOne(
namespace=namespace,
document={"a": "b"},
)
)
self.addCleanup(client.db["coll"].drop)
self.addCleanup(client.db[c_repeated].drop)
# Batch splitting required.
result = client.bulk_write(models=models)
self.assertEqual(result.inserted_count, num_models + 1)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)
first_event, second_event = bulk_write_events
self.assertEqual(len(first_event.command["ops"]), num_models)
self.assertEqual(len(first_event.command["nsInfo"]), 1)
self.assertEqual(first_event.command["nsInfo"][0]["ns"], "db.coll")
self.assertEqual(len(second_event.command["ops"]), 1)
self.assertEqual(len(second_event.command["nsInfo"]), 1)
self.assertEqual(second_event.command["nsInfo"][0]["ns"], namespace)
@client_context.require_version_min(8, 0, 0, -24)
def test_returns_error_if_no_writes_can_be_added_to_ops(self):
client = rs_or_single_client()
self.addCleanup(client.close)
max_message_size_bytes = (client_context.hello)["maxMessageSizeBytes"]
# Document too large.
b_repeated = "b" * max_message_size_bytes
models = [InsertOne(namespace="db.coll", document={"a": b_repeated})]
with self.assertRaises(InvalidOperation) as context:
client.bulk_write(models=models)
self.assertIn("cannot do an empty bulk write", context.exception._message)
# Namespace too large.
c_repeated = "c" * max_message_size_bytes
namespace = f"db.{c_repeated}"
models = [InsertOne(namespace=namespace, document={"a": "b"})]
with self.assertRaises(InvalidOperation) as context:
client.bulk_write(models=models)
self.assertIn("cannot do an empty bulk write", context.exception._message)
@client_context.require_version_min(8, 0, 0, -24)
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
def test_returns_error_if_auto_encryption_configured(self):
opts = AutoEncryptionOpts(
key_vault_namespace="db.coll",
kms_providers={"aws": {"accessKeyId": "foo", "secretAccessKey": "bar"}},
)
client = rs_or_single_client(auto_encryption_opts=opts)
self.addCleanup(client.close)
models = [InsertOne(namespace="db.coll", document={"a": "b"})]
with self.assertRaises(InvalidOperation) as context:
client.bulk_write(models=models)
self.assertIn(
"bulk_write does not currently support automatic encryption", context.exception._message
)
# https://github.com/mongodb/specifications/blob/master/source/client-side-operations-timeout/tests/README.md#11-multi-batch-bulkwrites
class TestClientBulkWriteTimeout(IntegrationTest):
@client_context.require_version_min(8, 0, 0, -24)
@client_context.require_failCommand_fail_point
def test_timeout_in_multi_batch_bulk_write(self):
internal_client = rs_or_single_client(timeoutMS=None)
self.addCleanup(internal_client.close)
collection = internal_client.db["coll"]
self.addCleanup(collection.drop)
collection.drop()
max_bson_object_size = (client_context.hello)["maxBsonObjectSize"]
max_message_size_bytes = (client_context.hello)["maxMessageSizeBytes"]
fail_command = {
"configureFailPoint": "failCommand",
"mode": {"times": 2},
"data": {"failCommands": ["bulkWrite"], "blockConnection": True, "blockTimeMS": 1010},
}
with self.fail_point(fail_command):
models = []
num_models = int(max_message_size_bytes / max_bson_object_size + 1)
b_repeated = "b" * (max_bson_object_size - 500)
for _ in range(num_models):
models.append(
InsertOne(
namespace="db.coll",
document={"a": b_repeated},
)
)
listener = OvertCommandListener()
client = rs_or_single_client(
event_listeners=[listener],
readConcernLevel="majority",
readPreference="primary",
timeoutMS=2000,
w="majority",
)
self.addCleanup(client.close)
with self.assertRaises(ClientBulkWriteException) as context:
client.bulk_write(models=models)
self.assertIsInstance(context.exception.error, NetworkTimeout)
bulk_write_events = []
for event in listener.started_events:
if event.command_name == "bulkWrite":
bulk_write_events.append(event)
self.assertEqual(len(bulk_write_events), 2)

View File

@ -232,7 +232,7 @@ class TestCursor(IntegrationTest):
listener = AllowListEventListener("find", "getMore")
coll = (rs_or_single_client(event_listeners=[listener]))[self.db.name].pymongo_test
# Tailable_await defaults.
# Tailable_defaults.
coll.find(cursor_type=CursorType.TAILABLE_AWAIT).to_list()
# find
self.assertFalse("maxTimeMS" in listener.started_events[0].command)
@ -240,7 +240,7 @@ class TestCursor(IntegrationTest):
self.assertFalse("maxTimeMS" in listener.started_events[1].command)
listener.reset()
# Tailable_await with max_await_time_ms set.
# Tailable_with max_await_time_ms set.
coll.find(cursor_type=CursorType.TAILABLE_AWAIT).max_await_time_ms(99).to_list()
# find
self.assertEqual("find", listener.started_events[0].command_name)
@ -251,7 +251,7 @@ class TestCursor(IntegrationTest):
self.assertEqual(99, listener.started_events[1].command["maxTimeMS"])
listener.reset()
# Tailable_await with max_time_ms and make sure list() works on synchronous cursors
# Tailable_with max_time_ms and make sure list() works on synchronous cursors
if _IS_SYNC:
list(coll.find(cursor_type=CursorType.TAILABLE_AWAIT).max_time_ms(99)) # type: ignore[call-overload]
else:
@ -265,7 +265,7 @@ class TestCursor(IntegrationTest):
self.assertFalse("maxTimeMS" in listener.started_events[1].command)
listener.reset()
# Tailable_await with both max_time_ms and max_await_time_ms
# Tailable_with both max_time_ms and max_await_time_ms
(
coll.find(cursor_type=CursorType.TAILABLE_AWAIT)
.max_time_ms(99)
@ -1371,41 +1371,39 @@ class TestCursor(IntegrationTest):
self.assertEqual("getMore", started[1].command_name)
self.assertNotIn("$readPreference", started[1].command)
@client_context.require_version_min(4, 0)
@client_context.require_replica_set
def test_to_list_tailable(self):
oplog = self.client.local.oplog.rs
last = oplog.find().sort("$natural", pymongo.DESCENDING).limit(-1).next()
ts = last["ts"]
# Set maxAwaitTimeMS=1 to speed up the test and avoid blocking on the noop writer.
c = oplog.find(
{"ts": {"$gte": ts}}, cursor_type=pymongo.CursorType.TAILABLE_AWAIT, oplog_replay=True
)
).max_await_time_ms(1)
self.addCleanup(c.close)
docs = c.to_list()
self.assertGreaterEqual(len(docs), 1)
def test_to_list_empty(self):
c = self.db.does_not_exist.find()
docs = c.to_list()
self.assertEqual([], docs)
@client_context.require_replica_set
@client_context.require_change_streams
def test_command_cursor_to_list(self):
c = self.db.test.aggregate([{"$changeStream": {}}])
# Set maxAwaitTimeMS=1 to speed up the test.
c = self.db.test.aggregate([{"$changeStream": {}}], maxAwaitTimeMS=1)
self.addCleanup(c.close)
docs = c.to_list()
self.assertGreaterEqual(len(docs), 0)
@client_context.require_replica_set
@client_context.require_change_streams
def test_command_cursor_to_list_empty(self):
c = self.db.does_not_exist.aggregate([{"$changeStream": {}}])
# Set maxAwaitTimeMS=1 to speed up the test.
c = self.db.does_not_exist.aggregate([{"$changeStream": {}}], maxAwaitTimeMS=1)
self.addCleanup(c.close)
docs = c.to_list()
self.assertEqual([], docs)

View File

@ -764,9 +764,7 @@ class TestGridFileCustomType(IntegrationTest):
db.fs,
_id=5,
filename="my_file",
contentType="text/html",
chunkSize=1000,
aliases=["foo"],
metadata={"foo": "red", "bar": "blue"},
bar=3,
baz="hello",
@ -780,13 +778,10 @@ class TestGridFileCustomType(IntegrationTest):
self.assertEqual("my_file", two.filename)
self.assertEqual(5, two._id)
self.assertEqual(11, two.length)
self.assertEqual("text/html", two.content_type)
self.assertEqual(1000, two.chunk_size)
self.assertTrue(isinstance(two.upload_date, datetime.datetime))
self.assertEqual(["foo"], two.aliases)
self.assertEqual({"foo": "red", "bar": "blue"}, two.metadata)
self.assertEqual(3, two.bar)
self.assertEqual(None, two.md5)
for attr in [
"_id",
@ -805,7 +800,9 @@ class TestGridFileCustomType(IntegrationTest):
class ChangeStreamsWCustomTypesTestMixin:
@no_type_check
def change_stream(self, *args, **kwargs):
return self.watched_target.watch(*args, **kwargs)
stream = self.watched_target.watch(*args, max_await_time_ms=1, **kwargs)
self.addCleanup(stream.close)
return stream
@no_type_check
def insert_and_check(self, change_stream, insert_doc, expected_doc):

View File

@ -17,16 +17,16 @@ from __future__ import annotations
import os
import sys
from pathlib import Path
import pytest
sys.path[0:0] = [""]
from test import IntegrationTest, client_context, unittest
from test.crud_v2_format import TestCrudV2
from test.unified_format import generate_test_classes
from test.utils import (
OvertCommandListener,
SpecTestCreator,
rs_client_noauth,
rs_or_single_client,
)
@ -100,30 +100,11 @@ class TestDataLakeProse(IntegrationTest):
client[self.TEST_DB][self.TEST_COLLECTION].find_one()
class DataLakeTestSpec(TestCrudV2):
# Default test database and collection names.
TEST_DB = "test"
TEST_COLLECTION = "driverdata"
# Location of JSON test specifications.
TEST_PATH = Path(__file__).parent / "data_lake/unified"
@classmethod
@client_context.require_data_lake
def setUpClass(cls):
super().setUpClass()
def setup_scenario(self, scenario_def):
# Spec tests MUST NOT insert data/drop collection for
# data lake testing.
pass
def create_test(scenario_def, test, name):
def run_scenario(self):
self.run_scenario(scenario_def, test)
return run_scenario
SpecTestCreator(create_test, DataLakeTestSpec, _TEST_PATH).create_tests()
# Generate unified tests.
globals().update(generate_test_classes(TEST_PATH, module=__name__))
if __name__ == "__main__":

View File

@ -747,6 +747,7 @@ class TestSampleShellCommands(IntegrationTest):
done = False
def insert_docs():
nonlocal done
while not done:
db.inventory.insert_one({"username": "alice"})
db.inventory.delete_one({"username": "alice"})
@ -760,17 +761,20 @@ class TestSampleShellCommands(IntegrationTest):
cursor = db.inventory.watch()
next(cursor)
# End Changestream Example 1
cursor.close()
# Start Changestream Example 2
cursor = db.inventory.watch(full_document="updateLookup")
next(cursor)
# End Changestream Example 2
cursor.close()
# Start Changestream Example 3
resume_token = cursor.resume_token
cursor = db.inventory.watch(resume_after=resume_token)
next(cursor)
# End Changestream Example 3
cursor.close()
# Start Changestream Example 4
pipeline = [
@ -780,6 +784,7 @@ class TestSampleShellCommands(IntegrationTest):
cursor = db.inventory.watch(pipeline=pipeline)
next(cursor)
# End Changestream Example 4
cursor.close()
finally:
done = True
t.join()

View File

@ -416,7 +416,8 @@ class TestPooling(_TestPoolingBase):
@client_context.require_failCommand_fail_point
def test_csot_timeout_message(self):
client = rs_or_single_client(appName="connectionTimeoutApp")
# Mock a connection failing due to timeout.
self.addCleanup(client.close)
# Mock an operation failing due to pymongo.timeout().
mock_connection_timeout = {
"configureFailPoint": "failCommand",
"mode": "alwaysOn",
@ -440,8 +441,8 @@ class TestPooling(_TestPoolingBase):
@client_context.require_failCommand_fail_point
def test_socket_timeout_message(self):
client = rs_or_single_client(socketTimeoutMS=500, appName="connectionTimeoutApp")
# Mock a connection failing due to timeout.
self.addCleanup(client.close)
# Mock an operation failing due to socketTimeoutMS.
mock_connection_timeout = {
"configureFailPoint": "failCommand",
"mode": "alwaysOn",
@ -469,7 +470,7 @@ class TestPooling(_TestPoolingBase):
4, 9, 0
) # configureFailPoint does not allow failure on handshake before 4.9, fixed in SERVER-49336
def test_connection_timeout_message(self):
# Mock a connection failing due to timeout.
# Mock a connection creation failing due to timeout.
mock_connection_timeout = {
"configureFailPoint": "failCommand",
"mode": "alwaysOn",
@ -481,9 +482,18 @@ class TestPooling(_TestPoolingBase):
},
}
client = rs_or_single_client(
connectTimeoutMS=500,
socketTimeoutMS=500,
appName="connectionTimeoutApp",
heartbeatFrequencyMS=1000000,
)
self.addCleanup(client.close)
client.admin.command("ping")
pool = get_pool(client)
pool.reset_without_pause()
with self.fail_point(mock_connection_timeout):
with self.assertRaises(Exception) as error:
client = rs_or_single_client(connectTimeoutMS=500, appName="connectionTimeoutApp")
client.admin.command("ping")
self.assertTrue(

View File

@ -23,14 +23,13 @@ import warnings
sys.path[0:0] = [""]
from test import IntegrationTest, client_context, unittest
from test.unified_format import generate_test_classes
from test.utils import (
EventListener,
SpecTestCreator,
disable_replication,
enable_replication,
rs_or_single_client,
)
from test.utils_spec_runner import SpecRunner
from pymongo import DESCENDING
from pymongo.errors import (
@ -321,25 +320,15 @@ def create_tests():
create_tests()
class TestOperation(SpecRunner):
# Location of JSON test specifications.
TEST_PATH = os.path.join(_TEST_PATH, "operation")
def get_outcome_coll_name(self, outcome, collection):
"""Spec says outcome has an optional 'collection.name'."""
return outcome["collection"].get("name", collection.name)
def create_operation_test(scenario_def, test, name):
@client_context.require_test_commands
def run_scenario(self):
self.run_scenario(scenario_def, test)
return run_scenario
test_creator = SpecTestCreator(create_operation_test, TestOperation, TestOperation.TEST_PATH)
test_creator.create_tests()
# Generate unified tests.
# PyMongo does not support MapReduce.
globals().update(
generate_test_classes(
os.path.join(_TEST_PATH, "operation"),
module=__name__,
expected_failures=["MapReduce .*"],
)
)
if __name__ == "__main__":

Some files were not shown because too many files have changed in this diff Show More