Skip to content

Commit 8419ae4

Browse files
authored
docs: add test for using FOR UPDATE (#575)
Spanner now supports FOR UPDATE clauses. This change adds a test to verify that FOR UPDATE clauses can be generated with the Spanner SQLAlchemy provider. See also https://cloud.google.com/spanner/docs/release-notes#January_27_2025
1 parent 8d9d045 commit 8419ae4

File tree

4 files changed

+92
-102
lines changed

4 files changed

+92
-102
lines changed

test/mockserver_tests/mock_server_test_base.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
1415
from google.cloud.spanner_dbapi.parsed_statement import AutocommitDmlMode
1516
from sqlalchemy import Engine, create_engine
1617
from sqlalchemy.testing.plugin.plugin_base import fixtures
@@ -79,6 +80,52 @@ def add_single_result(
7980
MockServerTestBase.spanner_service.mock_spanner.add_result(sql, result)
8081

8182

83+
def add_singer_query_result(sql: str):
84+
result = result_set.ResultSet(
85+
dict(
86+
metadata=result_set.ResultSetMetadata(
87+
dict(
88+
row_type=spanner_type.StructType(
89+
dict(
90+
fields=[
91+
spanner_type.StructType.Field(
92+
dict(
93+
name="singers_id",
94+
type=spanner_type.Type(
95+
dict(code=spanner_type.TypeCode.INT64)
96+
),
97+
)
98+
),
99+
spanner_type.StructType.Field(
100+
dict(
101+
name="singers_name",
102+
type=spanner_type.Type(
103+
dict(code=spanner_type.TypeCode.STRING)
104+
),
105+
)
106+
),
107+
]
108+
)
109+
)
110+
)
111+
),
112+
)
113+
)
114+
result.rows.extend(
115+
[
116+
(
117+
"1",
118+
"Jane Doe",
119+
),
120+
(
121+
"2",
122+
"John Doe",
123+
),
124+
]
125+
)
126+
add_result(sql, result)
127+
128+
82129
class MockServerTestBase(fixtures.TestBase):
83130
server: grpc.Server = None
84131
spanner_service: SpannerServicer = None

test/mockserver_tests/test_basics.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
String,
2626
func,
2727
text,
28+
BigInteger,
2829
)
30+
from sqlalchemy.orm import Session, DeclarativeBase, Mapped, mapped_column
2931
from sqlalchemy.testing import eq_, is_instance_of
3032
from google.cloud.spanner_v1 import (
3133
FixedSizePool,
@@ -41,6 +43,7 @@
4143
add_result,
4244
add_single_result,
4345
add_update_count,
46+
add_singer_query_result,
4447
)
4548

4649

@@ -179,3 +182,35 @@ def test_partitioned_dml(self):
179182
)
180183
results = connection.execute(text(sql)).rowcount
181184
eq_(100, results)
185+
186+
def test_select_for_update(self):
187+
class Base(DeclarativeBase):
188+
pass
189+
190+
class Singer(Base):
191+
__tablename__ = "singers"
192+
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
193+
name: Mapped[str] = mapped_column(String)
194+
195+
query = (
196+
"SELECT singers.id AS singers_id, singers.name AS singers_name\n"
197+
"FROM singers\n"
198+
"WHERE singers.id = @a0\n"
199+
" LIMIT @a1 FOR UPDATE"
200+
)
201+
add_singer_query_result(query)
202+
update = "UPDATE singers SET name=@a0 WHERE singers.id = @a1"
203+
add_update_count(update, 1)
204+
205+
engine = create_engine(
206+
"spanner:///projects/p/instances/i/databases/d",
207+
connect_args={"client": self.client, "pool": FixedSizePool(size=10)},
208+
)
209+
210+
with Session(engine) as session:
211+
singer = (
212+
session.query(Singer).filter(Singer.id == 1).with_for_update().first()
213+
)
214+
singer.name = "New Name"
215+
session.add(singer)
216+
session.commit()

test/mockserver_tests/test_read_only_transaction.py

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
BeginTransactionRequest,
2323
TransactionOptions,
2424
)
25-
from test.mockserver_tests.mock_server_test_base import MockServerTestBase
26-
from test.mockserver_tests.mock_server_test_base import add_result
27-
import google.cloud.spanner_v1.types.type as spanner_type
28-
import google.cloud.spanner_v1.types.result_set as result_set
25+
from test.mockserver_tests.mock_server_test_base import (
26+
MockServerTestBase,
27+
add_singer_query_result,
28+
)
2929

3030

3131
class TestReadOnlyTransaction(MockServerTestBase):
@@ -71,49 +71,3 @@ def test_read_only_transaction(self):
7171
),
7272
begin_request.options,
7373
)
74-
75-
76-
def add_singer_query_result(sql: str):
77-
result = result_set.ResultSet(
78-
dict(
79-
metadata=result_set.ResultSetMetadata(
80-
dict(
81-
row_type=spanner_type.StructType(
82-
dict(
83-
fields=[
84-
spanner_type.StructType.Field(
85-
dict(
86-
name="singers_id",
87-
type=spanner_type.Type(
88-
dict(code=spanner_type.TypeCode.INT64)
89-
),
90-
)
91-
),
92-
spanner_type.StructType.Field(
93-
dict(
94-
name="singers_name",
95-
type=spanner_type.Type(
96-
dict(code=spanner_type.TypeCode.STRING)
97-
),
98-
)
99-
),
100-
]
101-
)
102-
)
103-
)
104-
),
105-
)
106-
)
107-
result.rows.extend(
108-
[
109-
(
110-
"1",
111-
"Jane Doe",
112-
),
113-
(
114-
"2",
115-
"John Doe",
116-
),
117-
]
118-
)
119-
add_result(sql, result)

test/mockserver_tests/test_stale_reads.py

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@
2323
BeginTransactionRequest,
2424
TransactionOptions,
2525
)
26-
from test.mockserver_tests.mock_server_test_base import MockServerTestBase
27-
from test.mockserver_tests.mock_server_test_base import add_result
28-
import google.cloud.spanner_v1.types.type as spanner_type
29-
import google.cloud.spanner_v1.types.result_set as result_set
26+
from test.mockserver_tests.mock_server_test_base import (
27+
MockServerTestBase,
28+
add_singer_query_result,
29+
)
3030

3131

3232
class TestStaleReads(MockServerTestBase):
3333
def test_stale_read_multi_use(self):
3434
from test.mockserver_tests.stale_read_model import Singer
3535

36-
add_singer_query_result("SELECT singers.id, singers.name \n" + "FROM singers")
36+
add_singer_query_result("SELECT singers.id, singers.name \nFROM singers")
3737
engine = create_engine(
3838
"spanner:///projects/p/instances/i/databases/d",
3939
echo=True,
@@ -82,7 +82,7 @@ def test_stale_read_multi_use(self):
8282
def test_stale_read_single_use(self):
8383
from test.mockserver_tests.stale_read_model import Singer
8484

85-
add_singer_query_result("SELECT singers.id, singers.name\n" + "FROM singers")
85+
add_singer_query_result("SELECT singers.id, singers.name \nFROM singers")
8686
engine = create_engine(
8787
"spanner:///projects/p/instances/i/databases/d",
8888
echo=True,
@@ -121,49 +121,3 @@ def test_stale_read_single_use(self):
121121
),
122122
execute_request.transaction.single_use,
123123
)
124-
125-
126-
def add_singer_query_result(sql: str):
127-
result = result_set.ResultSet(
128-
dict(
129-
metadata=result_set.ResultSetMetadata(
130-
dict(
131-
row_type=spanner_type.StructType(
132-
dict(
133-
fields=[
134-
spanner_type.StructType.Field(
135-
dict(
136-
name="singers_id",
137-
type=spanner_type.Type(
138-
dict(code=spanner_type.TypeCode.INT64)
139-
),
140-
)
141-
),
142-
spanner_type.StructType.Field(
143-
dict(
144-
name="singers_name",
145-
type=spanner_type.Type(
146-
dict(code=spanner_type.TypeCode.STRING)
147-
),
148-
)
149-
),
150-
]
151-
)
152-
)
153-
)
154-
),
155-
)
156-
)
157-
result.rows.extend(
158-
[
159-
(
160-
"1",
161-
"Jane Doe",
162-
),
163-
(
164-
"2",
165-
"John Doe",
166-
),
167-
]
168-
)
169-
add_result(sql, result)

0 commit comments

Comments
 (0)