2929 BeginTransactionRequest ,
3030 CommitRequest ,
3131 ExecuteSqlRequest ,
32+ RollbackRequest ,
3233 TypeCode ,
3334)
3435from 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