Skip to content

Commit fd64f4d

Browse files
committed
PYTHON-2030 Support collection and index creation in multi-doc transactions
1 parent c282cc1 commit fd64f4d

File tree

6 files changed

+514
-8
lines changed

6 files changed

+514
-8
lines changed

pymongo/client_session.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,13 @@
6565
If the block exits with an exception, the transaction automatically calls
6666
:meth:`ClientSession.abort_transaction`.
6767
68-
For multi-document transactions, you can only specify read/write (CRUD)
69-
operations on existing collections. For example, a multi-document transaction
70-
cannot include a create or drop collection/index operations, including an
68+
In general, multi-document transactions only support read/write (CRUD)
69+
operations on existing collections. However, MongoDB 4.4 adds support for
70+
creating collections and indexes with some limitations, including an
7171
insert operation that would result in the creation of a new collection.
72+
For a complete description of all the supported and unsupported operations
73+
see the `MongoDB server's documentation for transactions
74+
<http://dochub.mongodb.org/core/transactions>`_.
7275
7376
A session may only have a single active transaction at a time, multiple
7477
transactions on the same session can be executed in sequence.

pymongo/database.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,10 @@ def create_collection(self, name, codec_options=None,
390390
- `**kwargs` (optional): additional keyword arguments will
391391
be passed as options for the create collection command
392392
393+
.. versionchanged:: 3.11
394+
This method is now supported inside multi-document transactions
395+
with MongoDB 4.4+.
396+
393397
.. versionchanged:: 3.6
394398
Added ``session`` parameter.
395399
@@ -403,8 +407,11 @@ def create_collection(self, name, codec_options=None,
403407
Removed deprecated argument: options
404408
"""
405409
with self.__client._tmp_session(session) as s:
406-
if name in self.list_collection_names(
407-
filter={"name": name}, session=s):
410+
# Skip this check in a transaction where listCollections is not
411+
# supported.
412+
if ((not s or not s.in_transaction) and
413+
name in self.list_collection_names(
414+
filter={"name": name}, session=s)):
408415
raise CollectionInvalid("collection %s already exists" % name)
409416

410417
return Collection(self, name, True, codec_options,

test/test_transactions.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121

2222
from pymongo import client_session, WriteConcern
2323
from pymongo.client_session import TransactionOptions
24-
from pymongo.errors import (ConfigurationError,
24+
from pymongo.errors import (CollectionInvalid,
25+
ConfigurationError,
2526
ConnectionFailure,
2627
OperationFailure)
2728
from pymongo.operations import IndexModel, InsertOne
@@ -91,7 +92,6 @@ def test_transaction_options_validation(self):
9192
TypeError, "max_commit_time_ms must be an integer or None"):
9293
TransactionOptions(max_commit_time_ms="10000")
9394

94-
9595
@client_context.require_transactions
9696
def test_transaction_write_concern_override(self):
9797
"""Test txn overrides Client/Database/Collection write_concern."""
@@ -121,7 +121,6 @@ def test_transaction_write_concern_override(self):
121121

122122
unsupported_txn_writes = [
123123
(client.drop_database, [db.name], {}),
124-
(db.create_collection, ['collection'], {}),
125124
(db.drop_collection, ['collection'], {}),
126125
(coll.drop, [], {}),
127126
(coll.map_reduce,
@@ -135,6 +134,12 @@ def test_transaction_write_concern_override(self):
135134
(coll.drop_indexes, [], {}),
136135
(coll.aggregate, [[{"$out": "aggout"}]], {}),
137136
]
137+
# Creating a collection in a transaction requires MongoDB 4.4+.
138+
if client_context.version < (4, 3, 4):
139+
unsupported_txn_writes.extend([
140+
(db.create_collection, ['collection'], {}),
141+
])
142+
138143
for op in unsupported_txn_writes:
139144
op, args, kwargs = op
140145
with client.start_session() as s:
@@ -201,6 +206,30 @@ def test_unpin_for_non_transaction_operation(self):
201206

202207
self.assertGreater(len(addresses), 1)
203208

209+
@client_context.require_transactions
210+
@client_context.require_version_min(4, 3, 4)
211+
def test_create_collection(self):
212+
client = rs_client()
213+
self.addCleanup(client.close)
214+
db = client.pymongo_test
215+
coll = db.test_create_collection
216+
self.addCleanup(coll.drop)
217+
with client.start_session() as s, s.start_transaction():
218+
coll2 = db.create_collection(coll.name, session=s)
219+
self.assertEqual(coll, coll2)
220+
coll.insert_one({}, session=s)
221+
222+
# Outside a transaction we raise CollectionInvalid on existing colls.
223+
with self.assertRaises(CollectionInvalid):
224+
db.create_collection(coll.name)
225+
226+
# Inside a transaction we raise the OperationFailure from create.
227+
with client.start_session() as s:
228+
s.start_transaction()
229+
with self.assertRaises(OperationFailure) as ctx:
230+
db.create_collection(coll.name, session=s)
231+
self.assertEqual(ctx.exception.code, 48) # NamespaceExists
232+
204233

205234
class PatchSessionTimeout(object):
206235
"""Patches the client_session's with_transaction timeout for testing."""
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
{
2+
"runOn": [
3+
{
4+
"minServerVersion": "4.3.4",
5+
"topology": [
6+
"replicaset",
7+
"sharded"
8+
]
9+
}
10+
],
11+
"database_name": "transaction-tests",
12+
"collection_name": "test",
13+
"data": [],
14+
"tests": [
15+
{
16+
"description": "explicitly create collection using create command",
17+
"operations": [
18+
{
19+
"name": "dropCollection",
20+
"object": "database",
21+
"arguments": {
22+
"collection": "test"
23+
}
24+
},
25+
{
26+
"name": "startTransaction",
27+
"object": "session0"
28+
},
29+
{
30+
"name": "createCollection",
31+
"object": "database",
32+
"arguments": {
33+
"session": "session0",
34+
"collection": "test"
35+
}
36+
},
37+
{
38+
"name": "assertCollectionNotExists",
39+
"object": "testRunner",
40+
"arguments": {
41+
"database": "transaction-tests",
42+
"collection": "test"
43+
}
44+
},
45+
{
46+
"name": "commitTransaction",
47+
"object": "session0"
48+
},
49+
{
50+
"name": "assertCollectionExists",
51+
"object": "testRunner",
52+
"arguments": {
53+
"database": "transaction-tests",
54+
"collection": "test"
55+
}
56+
}
57+
],
58+
"expectations": [
59+
{
60+
"command_started_event": {
61+
"command": {
62+
"drop": "test",
63+
"writeConcern": null
64+
},
65+
"command_name": "drop",
66+
"database_name": "transaction-tests"
67+
}
68+
},
69+
{
70+
"command_started_event": {
71+
"command": {
72+
"create": "test",
73+
"lsid": "session0",
74+
"txnNumber": {
75+
"$numberLong": "1"
76+
},
77+
"startTransaction": true,
78+
"autocommit": false,
79+
"writeConcern": null
80+
},
81+
"command_name": "create",
82+
"database_name": "transaction-tests"
83+
}
84+
},
85+
{
86+
"command_started_event": {
87+
"command": {
88+
"commitTransaction": 1,
89+
"lsid": "session0",
90+
"txnNumber": {
91+
"$numberLong": "1"
92+
},
93+
"startTransaction": null,
94+
"autocommit": false,
95+
"writeConcern": null
96+
},
97+
"command_name": "commitTransaction",
98+
"database_name": "admin"
99+
}
100+
}
101+
]
102+
},
103+
{
104+
"description": "implicitly create collection using insert",
105+
"operations": [
106+
{
107+
"name": "dropCollection",
108+
"object": "database",
109+
"arguments": {
110+
"collection": "test"
111+
}
112+
},
113+
{
114+
"name": "startTransaction",
115+
"object": "session0"
116+
},
117+
{
118+
"name": "insertOne",
119+
"object": "collection",
120+
"arguments": {
121+
"session": "session0",
122+
"document": {
123+
"_id": 1
124+
}
125+
},
126+
"result": {
127+
"insertedId": 1
128+
}
129+
},
130+
{
131+
"name": "assertCollectionNotExists",
132+
"object": "testRunner",
133+
"arguments": {
134+
"database": "transaction-tests",
135+
"collection": "test"
136+
}
137+
},
138+
{
139+
"name": "commitTransaction",
140+
"object": "session0"
141+
},
142+
{
143+
"name": "assertCollectionExists",
144+
"object": "testRunner",
145+
"arguments": {
146+
"database": "transaction-tests",
147+
"collection": "test"
148+
}
149+
}
150+
],
151+
"expectations": [
152+
{
153+
"command_started_event": {
154+
"command": {
155+
"drop": "test",
156+
"writeConcern": null
157+
},
158+
"command_name": "drop",
159+
"database_name": "transaction-tests"
160+
}
161+
},
162+
{
163+
"command_started_event": {
164+
"command": {
165+
"insert": "test",
166+
"documents": [
167+
{
168+
"_id": 1
169+
}
170+
],
171+
"ordered": true,
172+
"readConcern": null,
173+
"lsid": "session0",
174+
"txnNumber": {
175+
"$numberLong": "1"
176+
},
177+
"startTransaction": true,
178+
"autocommit": false,
179+
"writeConcern": null
180+
},
181+
"command_name": "insert",
182+
"database_name": "transaction-tests"
183+
}
184+
},
185+
{
186+
"command_started_event": {
187+
"command": {
188+
"commitTransaction": 1,
189+
"lsid": "session0",
190+
"txnNumber": {
191+
"$numberLong": "1"
192+
},
193+
"startTransaction": null,
194+
"autocommit": false,
195+
"writeConcern": null
196+
},
197+
"command_name": "commitTransaction",
198+
"database_name": "admin"
199+
}
200+
}
201+
]
202+
}
203+
]
204+
}

0 commit comments

Comments
 (0)