Skip to content

Commit 36fbef3

Browse files
committed
test(spanner): replace DB-API DDL system tests with mock server tests
Replaces live-instance DDL system tests in `test_dbapi.py` with fast, deterministic in-memory mock server tests in `test_dbapi_ddl.py`. Previously, `test_dbapi.py` dynamically created and dropped ephemeral tables (e.g. `Singers`, `DdlExecute`, `DdlExecuteMany`, `JsonDetails`) against live Cloud Spanner instances to verify client-side DDL buffering and flushing. Each table creation/drop triggered a long-running operation (LRO), adding ~1.5 to 2.0 minutes of latency and causing test flakiness. Changes: - Add `test_dbapi_ddl.py` in `tests/mockserver_tests/` with 9 mock server tests covering immediate autocommit DDL execution, non-autocommit DDL buffering, multi-statement DDL batches, DDL flushing before subsequent queries and DML, and client-side rejection of DDL in `executemany()`. - Remove 7 redundant DDL tests from `tests/system/test_dbapi.py`. - Update `test_autocommit_with_json_data` in `test_dbapi.py` to reuse the existing `all_types` table with row cleanup instead of creating and dropping a dedicated table. - Eliminate all dynamic table churn in DB-API system tests, saving ~1.5 to 2.0 minutes in standard CI while executing the replacement mock tests in < 1 second.
1 parent 0d89f73 commit 36fbef3

2 files changed

Lines changed: 253 additions & 261 deletions

File tree

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# Copyright 2026 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from google.cloud.spanner_dbapi.connection import Connection
16+
from google.cloud.spanner_dbapi.exceptions import ProgrammingError
17+
from google.cloud.spanner_v1 import ExecuteBatchDmlRequest, ExecuteSqlRequest
18+
from tests.mockserver_tests.mock_server_test_base import (
19+
MockServerTestBase,
20+
add_select1_result,
21+
add_update_count,
22+
)
23+
24+
25+
class TestDbapiDdl(MockServerTestBase):
26+
def test_ddl_autocommit(self):
27+
connection = Connection(self.instance, self.database)
28+
connection.autocommit = True
29+
ddl_statement = (
30+
"CREATE TABLE Singers ("
31+
"SingerId INT64 NOT NULL, "
32+
"Name STRING(1024)"
33+
") PRIMARY KEY (SingerId)"
34+
)
35+
36+
try:
37+
with connection.cursor() as cursor:
38+
cursor.execute(ddl_statement)
39+
40+
self.assertEqual(1, len(self.database_admin_service.requests))
41+
request = self.database_admin_service.requests[0]
42+
self.assertEqual(self.database.name, request.database)
43+
self.assertEqual([ddl_statement], request.statements)
44+
finally:
45+
connection.close()
46+
47+
def test_ddl_buffered_until_commit(self):
48+
connection = Connection(self.instance, self.database)
49+
connection.autocommit = False
50+
ddl_statement = (
51+
"CREATE TABLE Singers ("
52+
"SingerId INT64 NOT NULL, "
53+
"Name STRING(1024)"
54+
") PRIMARY KEY (SingerId)"
55+
)
56+
57+
try:
58+
with connection.cursor() as cursor:
59+
cursor.execute(ddl_statement)
60+
61+
self.assertEqual(0, len(self.database_admin_service.requests))
62+
connection.commit()
63+
64+
self.assertEqual(1, len(self.database_admin_service.requests))
65+
request = self.database_admin_service.requests[0]
66+
self.assertEqual(self.database.name, request.database)
67+
self.assertEqual([ddl_statement], request.statements)
68+
finally:
69+
connection.close()
70+
71+
def test_ddl_multiple_statements_buffered_until_commit(self):
72+
connection = Connection(self.instance, self.database)
73+
connection.autocommit = False
74+
statement1 = "CREATE TABLE Table1 (Id INT64) PRIMARY KEY (Id)"
75+
statement2 = "CREATE TABLE Table2 (Id INT64) PRIMARY KEY (Id)"
76+
77+
try:
78+
with connection.cursor() as cursor:
79+
cursor.execute(statement1)
80+
cursor.execute(statement2)
81+
82+
self.assertEqual(0, len(self.database_admin_service.requests))
83+
connection.commit()
84+
85+
self.assertEqual(1, len(self.database_admin_service.requests))
86+
request = self.database_admin_service.requests[0]
87+
self.assertEqual(self.database.name, request.database)
88+
self.assertEqual([statement1, statement2], request.statements)
89+
finally:
90+
connection.close()
91+
92+
def test_ddl_semicolon_separated_statements_autocommit(self):
93+
connection = Connection(self.instance, self.database)
94+
connection.autocommit = True
95+
statement1 = "CREATE TABLE Table1 (Id INT64) PRIMARY KEY (Id)"
96+
statement2 = "CREATE TABLE Table2 (Id INT64) PRIMARY KEY (Id)"
97+
98+
try:
99+
with connection.cursor() as cursor:
100+
cursor.execute(f"{statement1}; {statement2}")
101+
102+
self.assertEqual(1, len(self.database_admin_service.requests))
103+
request = self.database_admin_service.requests[0]
104+
self.assertEqual(self.database.name, request.database)
105+
self.assertEqual([statement1, statement2], request.statements)
106+
finally:
107+
connection.close()
108+
109+
def test_ddl_flushed_before_subsequent_execute(self):
110+
dml_statement = (
111+
"INSERT INTO Singers (SingerId, Name) VALUES (1, 'first-name')"
112+
)
113+
add_update_count(dml_statement, 1)
114+
115+
connection = Connection(self.instance, self.database)
116+
connection.autocommit = False
117+
ddl_statement = (
118+
"CREATE TABLE Singers ("
119+
"SingerId INT64 NOT NULL, "
120+
"Name STRING(1024)"
121+
") PRIMARY KEY (SingerId)"
122+
)
123+
124+
try:
125+
with connection.cursor() as cursor:
126+
cursor.execute(ddl_statement)
127+
self.assertEqual(0, len(self.database_admin_service.requests))
128+
129+
cursor.execute(dml_statement)
130+
self.assertEqual(1, len(self.database_admin_service.requests))
131+
request = self.database_admin_service.requests[0]
132+
self.assertEqual(self.database.name, request.database)
133+
self.assertEqual([ddl_statement], request.statements)
134+
self.assertEqual(1, cursor.rowcount)
135+
136+
execute_requests = [
137+
req
138+
for req in self.spanner_service.requests
139+
if isinstance(req, ExecuteSqlRequest)
140+
]
141+
self.assertEqual(1, len(execute_requests))
142+
self.assertEqual(dml_statement, execute_requests[0].sql)
143+
144+
connection.commit()
145+
finally:
146+
connection.close()
147+
148+
def test_ddl_flushed_before_subsequent_executemany(self):
149+
dml_statement = (
150+
"INSERT INTO Singers (SingerId, Name) VALUES (1, 'first-name')"
151+
)
152+
add_update_count(dml_statement, 1)
153+
154+
connection = Connection(self.instance, self.database)
155+
connection.autocommit = False
156+
ddl_statement = (
157+
"CREATE TABLE Singers ("
158+
"SingerId INT64 NOT NULL, "
159+
"Name STRING(1024)"
160+
") PRIMARY KEY (SingerId)"
161+
)
162+
163+
try:
164+
with connection.cursor() as cursor:
165+
cursor.execute(ddl_statement)
166+
self.assertEqual(0, len(self.database_admin_service.requests))
167+
168+
cursor.executemany(dml_statement, [()])
169+
self.assertEqual(1, len(self.database_admin_service.requests))
170+
request = self.database_admin_service.requests[0]
171+
self.assertEqual(self.database.name, request.database)
172+
self.assertEqual([ddl_statement], request.statements)
173+
self.assertEqual(1, cursor.rowcount)
174+
175+
batch_requests = [
176+
req
177+
for req in self.spanner_service.requests
178+
if isinstance(req, ExecuteBatchDmlRequest)
179+
]
180+
self.assertEqual(1, len(batch_requests))
181+
self.assertEqual(
182+
dml_statement, batch_requests[0].statements[0].sql
183+
)
184+
185+
connection.commit()
186+
finally:
187+
connection.close()
188+
189+
def test_ddl_flushed_before_subsequent_query(self):
190+
add_select1_result()
191+
192+
connection = Connection(self.instance, self.database)
193+
connection.autocommit = False
194+
ddl_statement = (
195+
"CREATE TABLE Singers ("
196+
"SingerId INT64 NOT NULL, "
197+
"Name STRING(1024)"
198+
") PRIMARY KEY (SingerId)"
199+
)
200+
201+
try:
202+
with connection.cursor() as cursor:
203+
cursor.execute(ddl_statement)
204+
self.assertEqual(0, len(self.database_admin_service.requests))
205+
206+
cursor.execute("SELECT 1")
207+
self.assertEqual(1, len(self.database_admin_service.requests))
208+
request = self.database_admin_service.requests[0]
209+
self.assertEqual(self.database.name, request.database)
210+
self.assertEqual([ddl_statement], request.statements)
211+
212+
connection.commit()
213+
finally:
214+
connection.close()
215+
216+
def test_ddl_uncommitted_on_close(self):
217+
connection = Connection(self.instance, self.database)
218+
connection.autocommit = False
219+
ddl_statement = (
220+
"CREATE TABLE Singers ("
221+
"SingerId INT64 NOT NULL, "
222+
"Name STRING(1024)"
223+
") PRIMARY KEY (SingerId)"
224+
)
225+
226+
with connection.cursor() as cursor:
227+
cursor.execute(ddl_statement)
228+
229+
connection.close()
230+
self.assertEqual(0, len(self.database_admin_service.requests))
231+
232+
def test_ddl_executemany_rejected_client_side(self):
233+
connection = Connection(self.instance, self.database)
234+
ddl_statement = (
235+
"CREATE TABLE Singers (SingerId INT64) PRIMARY KEY (SingerId)"
236+
)
237+
238+
try:
239+
for autocommit in (False, True):
240+
connection.autocommit = autocommit
241+
with connection.cursor() as cursor:
242+
with self.assertRaises(ProgrammingError):
243+
cursor.executemany(ddl_statement, [])
244+
245+
self.assertEqual(0, len(self.database_admin_service.requests))
246+
finally:
247+
connection.close()

0 commit comments

Comments
 (0)