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

Commit 9b5877a

Browse files
committed
fix: Respect existing server default in alter column DDL
Include `DEFAULT (default_value)` stanzas in `ALTER COLUMN` DDL to avoid removing `DEFAULT` values when changing a column's type or nullability. Fixes: #732
1 parent c6c123f commit 9b5877a

3 files changed

Lines changed: 130 additions & 10 deletions

File tree

google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
ColumnType,
2121
alter_column,
2222
alter_table,
23+
format_server_default,
2324
format_type,
2425
)
2526
from google.api_core.client_options import ClientOptions
@@ -1853,11 +1854,14 @@ def do_execute_no_params(self, cursor, statement, context=None):
18531854
def visit_column_nullable(
18541855
element: "ColumnNullable", compiler: "SpannerDDLCompiler", **kw
18551856
) -> str:
1856-
return "%s %s %s %s" % (
1857-
alter_table(compiler, element.table_name, element.schema),
1858-
alter_column(compiler, element.column_name),
1859-
format_type(compiler, element.existing_type),
1860-
"" if element.nullable else "NOT NULL",
1857+
return _format_alter_column(
1858+
compiler,
1859+
element.table_name,
1860+
element.schema,
1861+
element.column_name,
1862+
element.existing_type,
1863+
element.nullable,
1864+
element.existing_server_default,
18611865
)
18621866

18631867

@@ -1866,9 +1870,28 @@ def visit_column_nullable(
18661870
def visit_column_type(
18671871
element: "ColumnType", compiler: "SpannerDDLCompiler", **kw
18681872
) -> str:
1869-
return "%s %s %s %s" % (
1870-
alter_table(compiler, element.table_name, element.schema),
1871-
alter_column(compiler, element.column_name),
1872-
"%s" % format_type(compiler, element.type_),
1873-
"" if element.existing_nullable else "NOT NULL",
1873+
return _format_alter_column(
1874+
compiler,
1875+
element.table_name,
1876+
element.schema,
1877+
element.column_name,
1878+
element.type_,
1879+
element.existing_nullable,
1880+
element.existing_server_default,
1881+
)
1882+
1883+
1884+
def _format_alter_column(
1885+
compiler, table_name, schema, column_name, type_, nullable, server_default
1886+
):
1887+
return "%s %s %s%s%s" % (
1888+
alter_table(compiler, table_name, schema),
1889+
alter_column(compiler, column_name),
1890+
format_type(compiler, type_),
1891+
"" if nullable else " NOT NULL",
1892+
(
1893+
""
1894+
if server_default is None
1895+
else f" DEFAULT {format_server_default(compiler, server_default)}"
1896+
),
18741897
)

test/unit/__init__.py

Whitespace-only changes.

test/unit/test_alembic.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Copyright 2025 Google LLC
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 alembic.ddl import base as ddl_base
16+
from google.cloud.sqlalchemy_spanner import sqlalchemy_spanner
17+
from sqlalchemy import String, TextClause
18+
from sqlalchemy.testing import eq_
19+
from sqlalchemy.testing.plugin.plugin_base import fixtures
20+
21+
22+
class TestAlembicTest(fixtures.TestBase):
23+
def test_visit_column_nullable_with_not_null_column(self):
24+
ddl = sqlalchemy_spanner.visit_column_nullable(
25+
ddl_base.ColumnNullable(
26+
name="tbl", column_name="col", nullable=False, existing_type=String(256)
27+
),
28+
sqlalchemy_spanner.SpannerDDLCompiler(
29+
sqlalchemy_spanner.SpannerDialect(), None
30+
),
31+
)
32+
eq_(ddl, "ALTER TABLE tbl ALTER COLUMN col STRING(256) NOT NULL")
33+
34+
def test_visit_column_nullable_with_nullable_column(self):
35+
ddl = sqlalchemy_spanner.visit_column_nullable(
36+
ddl_base.ColumnNullable(
37+
name="tbl", column_name="col", nullable=True, existing_type=String(256)
38+
),
39+
sqlalchemy_spanner.SpannerDDLCompiler(
40+
sqlalchemy_spanner.SpannerDialect(), None
41+
),
42+
)
43+
eq_(ddl, "ALTER TABLE tbl ALTER COLUMN col STRING(256)")
44+
45+
def test_visit_column_nullable_with_default(self):
46+
ddl = sqlalchemy_spanner.visit_column_nullable(
47+
ddl_base.ColumnNullable(
48+
name="tbl",
49+
column_name="col",
50+
nullable=False,
51+
existing_type=String(256),
52+
existing_server_default=TextClause("GENERATE_UUID()"),
53+
),
54+
sqlalchemy_spanner.SpannerDDLCompiler(
55+
sqlalchemy_spanner.SpannerDialect(), None
56+
),
57+
)
58+
eq_(
59+
ddl,
60+
"ALTER TABLE tbl "
61+
"ALTER COLUMN col "
62+
"STRING(256) NOT NULL DEFAULT (GENERATE_UUID())",
63+
)
64+
65+
def test_visit_column_type(self):
66+
ddl = sqlalchemy_spanner.visit_column_type(
67+
ddl_base.ColumnType(
68+
name="tbl",
69+
column_name="col",
70+
type_=String(256),
71+
existing_nullable=True,
72+
),
73+
sqlalchemy_spanner.SpannerDDLCompiler(
74+
sqlalchemy_spanner.SpannerDialect(), None
75+
),
76+
)
77+
eq_(ddl, "ALTER TABLE tbl ALTER COLUMN col STRING(256)")
78+
79+
def test_visit_column_type_with_default(self):
80+
ddl = sqlalchemy_spanner.visit_column_type(
81+
ddl_base.ColumnType(
82+
name="tbl",
83+
column_name="col",
84+
type_=String(256),
85+
existing_nullable=False,
86+
existing_server_default=TextClause("GENERATE_UUID()"),
87+
),
88+
sqlalchemy_spanner.SpannerDDLCompiler(
89+
sqlalchemy_spanner.SpannerDialect(), None
90+
),
91+
)
92+
eq_(
93+
ddl,
94+
"ALTER TABLE tbl "
95+
"ALTER COLUMN col "
96+
"STRING(256) NOT NULL DEFAULT (GENERATE_UUID())",
97+
)

0 commit comments

Comments
 (0)