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

Commit c6c123f

Browse files
committed
fix: Correctly Generate DDL for ALTER COLUMN ... SET DEFAULT
Alembic expects `get_column_default_string` to be implemented in order to use it for `ALTER TABLE.. ALTER COLUMN .. SET DEFAULT` DDL. In our case, this means wrapping the default value in parentheses. We implement `get_column_default_string` and have it add parentheses for use in both `CREATE TABLE` and `ALTER TABLE` DDL. Call path for alembic relying on `get_column_default_string` is here: https://github.com/sqlalchemy/alembic/blob/cd4f404358f101b2b930013c609c074baca61468/alembic/ddl/base.py#L252 https://github.com/sqlalchemy/alembic/blob/cd4f404358f101b2b930013c609c074baca61468/alembic/ddl/base.py#L315
1 parent 170379f commit c6c123f

File tree

3 files changed

+87
-1
lines changed

3 files changed

+87
-1
lines changed

google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@ def get_column_specification(self, column, **kwargs):
572572
elif has_identity:
573573
colspec += " " + self.process(column.identity)
574574
elif default is not None:
575-
colspec += " DEFAULT (" + default + ")"
575+
colspec += f" DEFAULT {default}"
576576
elif hasattr(column, "computed") and column.computed is not None:
577577
colspec += " " + self.process(column.computed)
578578

@@ -583,6 +583,13 @@ def get_column_specification(self, column, **kwargs):
583583

584584
return colspec
585585

586+
def get_column_default_string(self, column):
587+
default = super().get_column_default_string(column)
588+
if default is not None:
589+
return f"({default})"
590+
591+
return default
592+
586593
def visit_computed_column(self, generated, **kw):
587594
"""Computed column operator."""
588595
text = "AS (%s) STORED" % self.sql_compiler.process(
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 func
16+
from sqlalchemy.orm import DeclarativeBase
17+
from sqlalchemy.orm import Mapped
18+
from sqlalchemy.orm import mapped_column
19+
20+
21+
class Base(DeclarativeBase):
22+
pass
23+
24+
25+
class Singer(Base):
26+
__tablename__ = "singers"
27+
id: Mapped[str] = mapped_column(
28+
server_default=func.GENERATE_UUID(), primary_key=True
29+
)
30+
name: Mapped[str]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 FixedSizePool, ResultSet
18+
from test.mockserver_tests.mock_server_test_base import MockServerTestBase, add_result
19+
from google.cloud.spanner_admin_database_v1 import UpdateDatabaseDdlRequest
20+
21+
22+
class TestCreateTableDefault(MockServerTestBase):
23+
def test_create_table_with_default(self):
24+
from test.mockserver_tests.default_model import Base
25+
26+
add_result(
27+
"""SELECT true
28+
FROM INFORMATION_SCHEMA.TABLES
29+
WHERE TABLE_SCHEMA="" AND TABLE_NAME="singers"
30+
LIMIT 1
31+
""",
32+
ResultSet(),
33+
)
34+
engine = create_engine(
35+
"spanner:///projects/p/instances/i/databases/d",
36+
connect_args={"client": self.client, "pool": FixedSizePool(size=10)},
37+
)
38+
Base.metadata.create_all(engine)
39+
requests = self.database_admin_service.requests
40+
eq_(1, len(requests))
41+
is_instance_of(requests[0], UpdateDatabaseDdlRequest)
42+
eq_(1, len(requests[0].statements))
43+
eq_(
44+
"CREATE TABLE singers (\n"
45+
"\tid STRING(MAX) NOT NULL DEFAULT (GENERATE_UUID()), \n"
46+
"\tname STRING(MAX) NOT NULL\n"
47+
") PRIMARY KEY (id)",
48+
requests[0].statements[0],
49+
)

0 commit comments

Comments
 (0)