Skip to content

Commit 454900f

Browse files
committed
docs: add sample and update README
1 parent c876e40 commit 454900f

File tree

5 files changed

+112
-26
lines changed

5 files changed

+112
-26
lines changed

README.rst

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -293,29 +293,23 @@ This, however, may require to manually repeat a long list of operations, execute
293293

294294
In ``AUTOCOMMIT`` mode automatic transactions retry mechanism is disabled, as every operation is committed just in time, and there is no way an ``Aborted`` exception can happen.
295295

296-
Auto-incremented IDs
297-
~~~~~~~~~~~~~~~~~~~~
298-
299-
Cloud Spanner doesn't support autoincremented IDs mechanism due to
300-
performance reasons (`see for more
301-
details <https://cloud.google.com/spanner/docs/schema-design#primary-key-prevent-hotspots>`__).
302-
We recommend that you use the Python
303-
`uuid <https://docs.python.org/3/library/uuid.html>`__ module to
304-
generate primary key fields to avoid creating monotonically increasing
305-
keys.
306-
307-
Though it's not encouraged to do so, in case you *need* the feature, you
308-
can simulate it manually as follows:
309-
310-
.. code:: python
311-
312-
with engine.begin() as connection:
313-
top_id = connection.execute(
314-
select([user.c.user_id]).order_by(user.c.user_id.desc()).limit(1)
315-
).fetchone()
316-
next_id = top_id[0] + 1 if top_id else 1
317-
318-
connection.execute(user.insert(), {"user_id": next_id})
296+
Auto-increment primary keys
297+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
298+
299+
Spanner uses IDENTITY columns for auto-increment primary key values.
300+
IDENTITY columns use a backing bit-reversed sequence to generate unique
301+
values that are safe to use as primary values in Spanner. These values
302+
work the same as standard auto-increment values, except that they are
303+
not monotonically increasing. This prevents hot-spotting for tables that
304+
receive a large number of writes.
305+
306+
`See this documentation page for more details <https://cloud.google.com/spanner/docs/schema-design#primary-key-prevent-hotspots>`__.
307+
308+
Auto-generated primary keys must be returned by Spanner after each insert
309+
statement using a ``THEN RETURN`` clause. ``THEN RETURN`` clauses are not
310+
supported with `Batch DML <https://cloud.google.com/spanner/docs/dml-tasks#use-batch>`__.
311+
It is therefore recommended to use for example client-side generated UUIDs
312+
as primary key values instead.
319313

320314
Query hints
321315
~~~~~~~~~~~
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright 2025 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+
import datetime
16+
import uuid
17+
18+
from sqlalchemy import create_engine
19+
from sqlalchemy.orm import Session
20+
21+
from sample_helper import run_sample
22+
from model import Singer, Concert, Venue, TicketSale
23+
24+
25+
# Shows how to use an IDENTITY column for primary key generation. IDENTITY
26+
# columns use a backing bit-reversed sequence to generate unique values that are
27+
# safe to use for primary keys in Spanner.
28+
#
29+
# IDENTITY columns are used by default by the Spanner SQLAlchemy dialect for
30+
# standard primary key columns.
31+
#
32+
# id: Mapped[int] = mapped_column(primary_key=True)
33+
#
34+
# This leads to the following table definition:
35+
#
36+
# CREATE TABLE ticket_sales (
37+
# id INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE),
38+
# ...
39+
# ) PRIMARY KEY (id)
40+
def auto_generated_primary_key_sample():
41+
engine = create_engine(
42+
"spanner:///projects/sample-project/"
43+
"instances/sample-instance/"
44+
"databases/sample-database",
45+
echo=True,
46+
)
47+
with Session(engine) as session:
48+
# Venue automatically generates a primary key value using an IDENTITY
49+
# column. We therefore do not need to specify a primary key value when
50+
# we create an instance of Venue.
51+
venue = Venue(code="CH", name="Concert Hall", active=True)
52+
session.add_all([venue])
53+
session.commit()
54+
55+
print("Inserted a venue with ID %d" % venue.id)
56+
57+
58+
if __name__ == "__main__":
59+
run_sample(auto_generated_primary_key_sample)

samples/model.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@
3131
ForeignKeyConstraint,
3232
Sequence,
3333
TextClause,
34-
func,
35-
FetchedValue,
34+
Index,
3635
)
3736
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
3837

@@ -45,6 +44,10 @@ class Base(DeclarativeBase):
4544
# This allows inserts to use Batch DML, as the primary key value does not need
4645
# to be returned from Spanner using a THEN RETURN clause.
4746
#
47+
# The Venue model uses a standard auto-generated integer primary key. This uses
48+
# an IDENTITY column in Spanner. IDENTITY columns use a backing bit-reversed
49+
# sequence to generate unique values that are safe to use for primary keys.
50+
#
4851
# The TicketSale model uses a bit-reversed sequence for primary key generation.
4952
# This is achieved by creating a bit-reversed sequence and assigning the id
5053
# column of the model a server_default value that gets the next value from that
@@ -117,7 +120,11 @@ class Track(Base):
117120

118121
class Venue(Base):
119122
__tablename__ = "venues"
120-
code: Mapped[str] = mapped_column(String(10), primary_key=True)
123+
__table_args__ = (Index("venues_code_unique", "code", unique=True),)
124+
# Venue uses a standard auto-generated primary key.
125+
# This translates to an IDENTITY column in Spanner.
126+
id: Mapped[int] = mapped_column(primary_key=True)
127+
code: Mapped[str] = mapped_column(String(10))
121128
name: Mapped[str] = mapped_column(String(200), nullable=False)
122129
description: Mapped[str] = mapped_column(JSON, nullable=True)
123130
active: Mapped[bool] = mapped_column(Boolean, nullable=False)

samples/noxfile.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ def hello_world(session):
2222
_sample(session)
2323

2424

25+
@nox.session()
26+
def auto_generated_primary_key(session):
27+
_sample(session)
28+
29+
2530
@nox.session()
2631
def bit_reversed_sequence(session):
2732
_sample(session)

test/mockserver_tests/test_auto_increment.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from test.mockserver_tests.mock_server_test_base import (
2727
MockServerTestBase,
2828
add_result,
29+
add_update_count,
2930
)
3031
from google.cloud.spanner_admin_database_v1 import UpdateDatabaseDdlRequest
3132
import google.cloud.spanner_v1.types.type as spanner_type
@@ -147,6 +148,26 @@ def test_insert_row(self):
147148
is_instance_of(requests[2], ExecuteSqlRequest)
148149
is_instance_of(requests[3], CommitRequest)
149150

151+
def test_insert_row_with_pk_value(self):
152+
from test.mockserver_tests.auto_increment_model import Singer
153+
154+
# SQLAlchemy should not use a THEN RETURN clause when a value for the
155+
# primary key has been set on the model.
156+
add_update_count("INSERT INTO singers (id, name) VALUES (@a0, @a1)", 1)
157+
engine = create_engine(
158+
"spanner:///projects/p/instances/i/databases/d",
159+
connect_args={"client": self.client, "pool": FixedSizePool(size=10)},
160+
)
161+
162+
with Session(engine) as session:
163+
# Manually specify a value for the primary key.
164+
singer = Singer(id=1, name="Test")
165+
session.add(singer)
166+
# Flush the session to send the insert statement to the database.
167+
session.flush()
168+
eq_(1, singer.id)
169+
session.commit()
170+
150171
def add_insert_result(self, sql):
151172
result = result_set.ResultSet(
152173
dict(

0 commit comments

Comments
 (0)