Skip to content

Commit 484374e

Browse files
authored
PYTHON-3298 Add flag to create_collection to skip listCollections pre-check (#1006)
1 parent bbe364f commit 484374e

File tree

6 files changed

+30
-32
lines changed

6 files changed

+30
-32
lines changed

doc/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ PyMongo 4.2 brings a number of improvements including:
1313
changes may be made before the final release. See :ref:`automatic-queryable-client-side-encryption` for example usage.
1414
- Provisional (beta) support for :func:`pymongo.timeout` to apply a single timeout
1515
to an entire block of pymongo operations.
16+
- Added ``check_exists`` option to :meth:`~pymongo.database.Database.create_collection`
17+
that when True (the default) runs an additional ``listCollections`` command to verify that the
18+
collection does not exist already.
1619

1720
Bug fixes
1821
.........

pymongo/database.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ def create_collection(
305305
read_concern: Optional["ReadConcern"] = None,
306306
session: Optional["ClientSession"] = None,
307307
timeout: Optional[float] = None,
308+
check_exists: Optional[bool] = True,
308309
**kwargs: Any,
309310
) -> Collection[_DocumentType]:
310311
"""Create a new :class:`~pymongo.collection.Collection` in this
@@ -336,6 +337,8 @@ def create_collection(
336337
:class:`~pymongo.collation.Collation`.
337338
- `session` (optional): a
338339
:class:`~pymongo.client_session.ClientSession`.
340+
- ``check_exists`` (optional): if True (the default), send a listCollections command to
341+
check if the collection already exists before creation.
339342
- `**kwargs` (optional): additional keyword arguments will
340343
be passed as options for the `create collection command`_
341344
@@ -402,7 +405,7 @@ def create_collection(
402405
enabling pre- and post-images.
403406
404407
.. versionchanged:: 4.2
405-
Added the ``clusteredIndex`` and ``encryptedFields`` parameters.
408+
Added the ``check_exists``, ``clusteredIndex``, and ``encryptedFields`` parameters.
406409
407410
.. versionchanged:: 3.11
408411
This method is now supported inside multi-document transactions
@@ -441,8 +444,10 @@ def create_collection(
441444
with self.__client._tmp_session(session) as s:
442445
# Skip this check in a transaction where listCollections is not
443446
# supported.
444-
if (not s or not s.in_transaction) and name in self.list_collection_names(
445-
filter={"name": name}, session=s
447+
if (
448+
check_exists
449+
and (not s or not s.in_transaction)
450+
and name in self.list_collection_names(filter={"name": name}, session=s)
446451
):
447452
raise CollectionInvalid("collection %s already exists" % name)
448453
return Collection(

test/test_database.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,21 @@ def test_list_collection_names_filter(self):
220220
self.assertIn("nameOnly", command)
221221
self.assertTrue(command["nameOnly"])
222222

223+
def test_check_exists(self):
224+
listener = OvertCommandListener()
225+
results = listener.results
226+
client = rs_or_single_client(event_listeners=[listener])
227+
self.addCleanup(client.close)
228+
db = client[self.db.name]
229+
db.drop_collection("unique")
230+
db.create_collection("unique", check_exists=True)
231+
self.assertIn("listCollections", listener.started_command_names())
232+
listener.reset()
233+
db.drop_collection("unique")
234+
db.create_collection("unique", check_exists=False)
235+
self.assertTrue(len(results["started"]) > 0)
236+
self.assertNotIn("listCollections", listener.started_command_names())
237+
223238
def test_list_collections(self):
224239
self.client.drop_database("pymongo_test")
225240
db = Database(self.client, "pymongo_test")

test/unified_format.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,6 @@ def __init__(
283283
self._observe_sensitive_commands = False
284284
self._ignore_commands = _SENSITIVE_COMMANDS | set(ignore_commands)
285285
self._ignore_commands.add("configurefailpoint")
286-
self.ignore_list_collections = False
287286
self._event_mapping = collections.defaultdict(list)
288287
self.entity_map = entity_map
289288
if store_events:
@@ -314,10 +313,7 @@ def add_event(self, event):
314313
)
315314

316315
def _command_event(self, event):
317-
if not (
318-
event.command_name.lower() in self._ignore_commands
319-
or (self.ignore_list_collections and event.command_name == "listCollections")
320-
):
316+
if not event.command_name.lower() in self._ignore_commands:
321317
self.add_event(event)
322318

323319
def started(self, event):
@@ -1032,13 +1028,8 @@ def _databaseOperation_listCollections(self, target, *args, **kwargs):
10321028

10331029
def _databaseOperation_createCollection(self, target, *args, **kwargs):
10341030
# PYTHON-1936 Ignore the listCollections event from create_collection.
1035-
for listener in target.client.options.event_listeners:
1036-
if isinstance(listener, EventListenerUtil):
1037-
listener.ignore_list_collections = True
1031+
kwargs["check_exists"] = False
10381032
ret = target.create_collection(*args, **kwargs)
1039-
for listener in target.client.options.event_listeners:
1040-
if isinstance(listener, EventListenerUtil):
1041-
listener.ignore_list_collections = False
10421033
return ret
10431034

10441035
def __entityOperation_aggregate(self, target, *args, **kwargs):

test/utils.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -202,23 +202,14 @@ class OvertCommandListener(EventListener):
202202
ignore_list_collections = False
203203

204204
def started(self, event):
205-
if self.ignore_list_collections and event.command_name.lower() == "listcollections":
206-
self.ignore_list_collections = False
207-
return
208205
if event.command_name.lower() not in _SENSITIVE_COMMANDS:
209206
super(OvertCommandListener, self).started(event)
210207

211208
def succeeded(self, event):
212-
if self.ignore_list_collections and event.command_name.lower() == "listcollections":
213-
self.ignore_list_collections = False
214-
return
215209
if event.command_name.lower() not in _SENSITIVE_COMMANDS:
216210
super(OvertCommandListener, self).succeeded(event)
217211

218212
def failed(self, event):
219-
if self.ignore_list_collections and event.command_name.lower() == "listcollections":
220-
self.ignore_list_collections = False
221-
return
222213
if event.command_name.lower() not in _SENSITIVE_COMMANDS:
223214
super(OvertCommandListener, self).failed(event)
224215

@@ -1114,6 +1105,7 @@ def prepare_spec_arguments(spec, arguments, opname, entity_map, with_txn_callbac
11141105
elif opname == "create_collection":
11151106
if arg_name == "collection":
11161107
arguments["name"] = arguments.pop(arg_name)
1108+
arguments["check_exists"] = False
11171109
# Any other arguments to create_collection are passed through
11181110
# **kwargs.
11191111
elif opname == "create_index" and arg_name == "keys":

test/utils_spec_runner.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -307,15 +307,7 @@ def run_operation(self, sessions, collection, operation):
307307
args.update(arguments)
308308
arguments = args
309309

310-
try:
311-
if name == "create_collection" and (
312-
"encrypted" in operation["arguments"]["name"]
313-
or "plaintext" in operation["arguments"]["name"]
314-
):
315-
self.listener.ignore_list_collections = True
316-
result = cmd(**dict(arguments))
317-
finally:
318-
self.listener.ignore_list_collections = False
310+
result = cmd(**dict(arguments))
319311
# Cleanup open change stream cursors.
320312
if name == "watch":
321313
self.addCleanup(result.close)

0 commit comments

Comments
 (0)