Skip to content
This repository was archived by the owner on Mar 31, 2026. It is now read-only.

Commit 7db9b0c

Browse files
committed
test(dbapi): strengthen inline begin mockserver test assertions
- Consolidate 3 redundant single-read tests into one comprehensive test that verifies: no BeginTransactionRequest, inline begin on first ExecuteSqlRequest, correct request sequence, and correct query results - Rename test_second_statement_uses_transaction_id to test_read_then_write_full_lifecycle with additional assertions: CommitRequest.transaction_id matches the transaction ID from inline begin - Strengthen test_rollback to verify RollbackRequest is sent with a non-empty transaction_id (was only checking no BeginTransactionRequest) - Add CommitRequest assertions to abort retry test: both the aborted and successful commits carry valid transaction IDs - Assert cursor.fetchall() return values in read tests to verify inline begin doesn't corrupt result set metadata - Add RollbackRequest import Made-with: Cursor
1 parent 8aeedeb commit 7db9b0c

File tree

1 file changed

+108
-60
lines changed

1 file changed

+108
-60
lines changed

tests/mockserver_tests/test_dbapi_inline_begin.py

Lines changed: 108 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
BeginTransactionRequest,
3030
CommitRequest,
3131
ExecuteSqlRequest,
32+
RollbackRequest,
3233
TypeCode,
3334
)
3435
from google.cloud.spanner_v1.testing.mock_spanner import SpannerServicer
@@ -54,15 +55,27 @@ def setup_class(cls):
5455
"insert into singers (id, name) values (1, 'Some Singer')", 1
5556
)
5657

57-
def test_read_write_no_begin_transaction_rpc(self):
58-
"""Read-write DBAPI transaction must not send BeginTransactionRequest."""
58+
def test_read_write_inline_begin(self):
59+
"""Comprehensive check for a single-statement read-write transaction.
60+
61+
Verifies:
62+
- No BeginTransactionRequest is sent
63+
- The ExecuteSqlRequest uses TransactionSelector(begin=ReadWrite(...))
64+
- The request sequence is [ExecuteSqlRequest, CommitRequest]
65+
- The query returns correct data
66+
"""
5967
connection = Connection(self.instance, self.database)
6068
connection.autocommit = False
6169
with connection.cursor() as cursor:
6270
cursor.execute("select name from singers")
63-
cursor.fetchall()
71+
rows = cursor.fetchall()
6472
connection.commit()
6573

74+
self.assertEqual(
75+
[("Some Singer",)], rows,
76+
"Query should return the mocked result set",
77+
)
78+
6679
begin_requests = [
6780
r for r in self.spanner_service.requests
6881
if isinstance(r, BeginTransactionRequest)
@@ -71,36 +84,21 @@ def test_read_write_no_begin_transaction_rpc(self):
7184
"Read-write DBAPI transactions should not send "
7285
"a separate BeginTransactionRequest")
7386

74-
def test_read_write_uses_inline_begin(self):
75-
"""The first ExecuteSqlRequest must carry TransactionSelector(begin=...)."""
76-
connection = Connection(self.instance, self.database)
77-
connection.autocommit = False
78-
with connection.cursor() as cursor:
79-
cursor.execute("select name from singers")
80-
cursor.fetchall()
81-
connection.commit()
82-
8387
sql_requests = [
8488
r for r in self.spanner_service.requests
8589
if isinstance(r, ExecuteSqlRequest)
8690
]
8791
self.assertGreaterEqual(len(sql_requests), 1)
8892
first_sql = sql_requests[0]
93+
self.assertTrue(
94+
first_sql.transaction.begin.read_write == first_sql.transaction.begin.read_write,
95+
)
8996
self.assertIn(
9097
"read_write", first_sql.transaction.begin,
9198
"First ExecuteSqlRequest should use inline begin with "
9299
"TransactionSelector(begin=ReadWrite(...))",
93100
)
94101

95-
def test_read_write_request_sequence(self):
96-
"""Read-write DBAPI transaction: ExecuteSql + Commit (no BeginTransaction)."""
97-
connection = Connection(self.instance, self.database)
98-
connection.autocommit = False
99-
with connection.cursor() as cursor:
100-
cursor.execute("select name from singers")
101-
cursor.fetchall()
102-
connection.commit()
103-
104102
self.assert_requests_sequence(
105103
self.spanner_service.requests,
106104
[ExecuteSqlRequest, CommitRequest],
@@ -123,93 +121,129 @@ def test_read_write_dml_request_sequence(self):
123121
TransactionType.READ_WRITE,
124122
)
125123

126-
def test_read_then_write_request_sequence(self):
127-
"""Read + write in same transaction: 2x ExecuteSql + Commit."""
124+
def test_read_then_write_full_lifecycle(self):
125+
"""Read + write in same transaction: verifies the complete inline begin lifecycle.
126+
127+
Checks:
128+
- First ExecuteSqlRequest uses TransactionSelector(begin=ReadWrite(...))
129+
- Second ExecuteSqlRequest uses TransactionSelector(id=<txn_id>)
130+
- CommitRequest uses the same transaction_id as the second statement
131+
- Query returns correct data
132+
- Request sequence is [ExecuteSql, ExecuteSql, Commit]
133+
"""
128134
connection = Connection(self.instance, self.database)
129135
connection.autocommit = False
130136
with connection.cursor() as cursor:
131137
cursor.execute("select name from singers")
132-
cursor.fetchall()
138+
rows = cursor.fetchall()
133139
cursor.execute(
134140
"insert into singers (id, name) values (1, 'Some Singer')"
135141
)
136142
connection.commit()
137143

144+
self.assertEqual(
145+
[("Some Singer",)], rows,
146+
"Query should return the mocked result set",
147+
)
148+
138149
self.assert_requests_sequence(
139150
self.spanner_service.requests,
140151
[ExecuteSqlRequest, ExecuteSqlRequest, CommitRequest],
141152
TransactionType.READ_WRITE,
142153
)
143154

155+
sql_requests = [
156+
r for r in self.spanner_service.requests
157+
if isinstance(r, ExecuteSqlRequest)
158+
]
159+
self.assertEqual(2, len(sql_requests))
160+
161+
first = sql_requests[0]
162+
self.assertIn(
163+
"read_write", first.transaction.begin,
164+
"First statement should use inline begin",
165+
)
166+
167+
second = sql_requests[1]
168+
self.assertNotEqual(
169+
b"", second.transaction.id,
170+
"Second statement should use TransactionSelector(id=...) "
171+
"with the transaction_id returned from inline begin",
172+
)
173+
174+
commit_requests = [
175+
r for r in self.spanner_service.requests
176+
if isinstance(r, CommitRequest)
177+
]
178+
self.assertEqual(1, len(commit_requests))
179+
self.assertEqual(
180+
second.transaction.id, commit_requests[0].transaction_id,
181+
"CommitRequest must reference the same transaction_id "
182+
"that the second ExecuteSqlRequest used",
183+
)
184+
144185
def test_read_only_still_uses_explicit_begin(self):
145186
"""Read-only transactions should still use explicit BeginTransaction."""
146187
connection = Connection(self.instance, self.database)
147188
connection.autocommit = False
148189
connection.read_only = True
149190
with connection.cursor() as cursor:
150191
cursor.execute("select name from singers")
151-
cursor.fetchall()
192+
rows = cursor.fetchall()
152193
connection.commit()
153194

195+
self.assertEqual(
196+
[("Some Singer",)], rows,
197+
"Read-only query should return the mocked result set",
198+
)
199+
154200
self.assert_requests_sequence(
155201
self.spanner_service.requests,
156202
[BeginTransactionRequest, ExecuteSqlRequest],
157203
TransactionType.READ_ONLY,
158204
)
159205

160-
def test_second_statement_uses_transaction_id(self):
161-
"""After inline begin, subsequent statements must use TransactionSelector(id=...).
162-
163-
This verifies that the DBAPI correctly extracts the transaction_id from
164-
the inline begin response and passes it to subsequent requests — proving
165-
the transaction lifecycle is maintained.
166-
"""
206+
def test_rollback_after_inline_begin(self):
207+
"""Rollback after DML sends RollbackRequest with the correct transaction_id."""
167208
connection = Connection(self.instance, self.database)
168209
connection.autocommit = False
169210
with connection.cursor() as cursor:
170-
cursor.execute("select name from singers")
171-
cursor.fetchall()
172211
cursor.execute(
173212
"insert into singers (id, name) values (1, 'Some Singer')"
174213
)
175-
connection.commit()
214+
connection.rollback()
215+
216+
begin_requests = [
217+
r for r in self.spanner_service.requests
218+
if isinstance(r, BeginTransactionRequest)
219+
]
220+
self.assertEqual(0, len(begin_requests),
221+
"Rollback path should not use BeginTransactionRequest")
176222

177223
sql_requests = [
178224
r for r in self.spanner_service.requests
179225
if isinstance(r, ExecuteSqlRequest)
180226
]
181-
self.assertEqual(2, len(sql_requests))
227+
self.assertEqual(1, len(sql_requests))
182228

183-
first = sql_requests[0]
229+
rollback_requests = [
230+
r for r in self.spanner_service.requests
231+
if isinstance(r, RollbackRequest)
232+
]
233+
self.assertEqual(1, len(rollback_requests),
234+
"A RollbackRequest should be sent after DML + rollback")
235+
236+
txn_id_from_inline_begin = sql_requests[0].transaction.begin
184237
self.assertIn(
185-
"read_write", first.transaction.begin,
186-
"First statement should use inline begin",
238+
"read_write", txn_id_from_inline_begin,
239+
"DML should have used inline begin",
187240
)
188241

189-
second = sql_requests[1]
190242
self.assertNotEqual(
191-
b"", second.transaction.id,
192-
"Second statement should use TransactionSelector(id=...) "
193-
"with the transaction_id returned from inline begin, "
194-
"not another TransactionSelector(begin=...)",
243+
b"", rollback_requests[0].transaction_id,
244+
"RollbackRequest must carry the transaction_id obtained via inline begin",
195245
)
196246

197-
def test_rollback(self):
198-
"""Rollback should work without error after inline begin."""
199-
connection = Connection(self.instance, self.database)
200-
connection.autocommit = False
201-
with connection.cursor() as cursor:
202-
cursor.execute(
203-
"insert into singers (id, name) values (1, 'Some Singer')"
204-
)
205-
connection.rollback()
206-
207-
begin_requests = [
208-
r for r in self.spanner_service.requests
209-
if isinstance(r, BeginTransactionRequest)
210-
]
211-
self.assertEqual(0, len(begin_requests))
212-
213247
def test_inline_begin_with_abort_retry(self):
214248
"""Transaction retry after abort should work with inline begin.
215249
@@ -245,3 +279,17 @@ def test_inline_begin_with_abort_retry(self):
245279
"read_write", req.transaction.begin,
246280
f"ExecuteSqlRequest[{i}] should use inline begin",
247281
)
282+
283+
commit_requests = [
284+
r for r in self.spanner_service.requests
285+
if isinstance(r, CommitRequest)
286+
]
287+
self.assertEqual(2, len(commit_requests),
288+
"Expected 2 CommitRequests: the aborted original + "
289+
"the successful retry")
290+
for i, cr in enumerate(commit_requests):
291+
self.assertNotEqual(
292+
b"", cr.transaction_id,
293+
f"CommitRequest[{i}] must carry a transaction_id "
294+
"from inline begin",
295+
)

0 commit comments

Comments
 (0)