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

Commit 3d363a1

Browse files
committed
feat: support commit timestamp option
Add support for columns with commit timestamps: https://cloud.google.com/spanner/docs/commit-timestamp Fixes: #695
1 parent 0f40a16 commit 3d363a1

File tree

4 files changed

+117
-0
lines changed

4 files changed

+117
-0
lines changed

README.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,22 @@ tables with this feature, make sure to call ``add_is_dependent_on()`` on
234234
the child table to request SQLAlchemy to create the parent table before
235235
the child table.
236236

237+
Commit timestamps
238+
~~~~~~~~~~~~~~~~~~
239+
240+
The dialect offers the ``spanner_allow_commit_timestamp`` option to
241+
column constructors for creating commit timestamp columns.
242+
243+
.. code:: python
244+
245+
Table(
246+
"table",
247+
metadata,
248+
Column("last_update_time", DateTime, spanner_allow_commit_timestamp=True),
249+
)
250+
251+
`See this documentation page for more details <https://cloud.google.com/spanner/docs/commit-timestamp>`__.
252+
237253
Unique constraints
238254
~~~~~~~~~~~~~~~~~~
239255

google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,11 @@ def get_column_specification(self, column, **kwargs):
578578
elif hasattr(column, "computed") and column.computed is not None:
579579
colspec += " " + self.process(column.computed)
580580

581+
if column.dialect_options.get("spanner", {}).get(
582+
"allow_commit_timestamp", False
583+
):
584+
colspec += " OPTIONS (allow_commit_timestamp=true)"
585+
581586
return colspec
582587

583588
def visit_computed_column(self, generated, **kw):
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
17+
from sqlalchemy.orm import DeclarativeBase
18+
from sqlalchemy.orm import Mapped
19+
from sqlalchemy.orm import mapped_column
20+
21+
22+
class Base(DeclarativeBase):
23+
pass
24+
25+
26+
class Singer(Base):
27+
__tablename__ = "singers"
28+
id: Mapped[str] = mapped_column(primary_key=True)
29+
name: Mapped[str]
30+
updated_at: Mapped[datetime.datetime] = mapped_column(
31+
spanner_allow_commit_timestamp=True
32+
)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
from sqlalchemy import create_engine
16+
from sqlalchemy.testing import eq_, is_instance_of
17+
from google.cloud.spanner_v1 import (
18+
FixedSizePool,
19+
ResultSet,
20+
)
21+
from test.mockserver_tests.mock_server_test_base import (
22+
MockServerTestBase,
23+
add_result,
24+
)
25+
from google.cloud.spanner_admin_database_v1 import UpdateDatabaseDdlRequest
26+
27+
28+
class TestCommitTimestamp(MockServerTestBase):
29+
def test_create_table(self):
30+
from test.mockserver_tests.commit_timestamp_model import Base
31+
32+
add_result(
33+
"""SELECT true
34+
FROM INFORMATION_SCHEMA.TABLES
35+
WHERE TABLE_SCHEMA="" AND TABLE_NAME="singers"
36+
LIMIT 1
37+
""",
38+
ResultSet(),
39+
)
40+
add_result(
41+
"""SELECT true
42+
FROM INFORMATION_SCHEMA.SEQUENCES
43+
WHERE NAME="singer_id"
44+
AND SCHEMA=""
45+
LIMIT 1""",
46+
ResultSet(),
47+
)
48+
engine = create_engine(
49+
"spanner:///projects/p/instances/i/databases/d",
50+
connect_args={"client": self.client, "pool": FixedSizePool(size=10)},
51+
)
52+
Base.metadata.create_all(engine)
53+
requests = self.database_admin_service.requests
54+
eq_(1, len(requests))
55+
is_instance_of(requests[0], UpdateDatabaseDdlRequest)
56+
eq_(1, len(requests[0].statements))
57+
eq_(
58+
"CREATE TABLE singers (\n"
59+
"\tid STRING(MAX) NOT NULL, \n"
60+
"\tname STRING(MAX) NOT NULL, \n"
61+
"\tupdated_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)\n"
62+
") PRIMARY KEY (id)",
63+
requests[0].statements[0],
64+
)

0 commit comments

Comments
 (0)