Skip to content

Commit 681f3de

Browse files
feat(sqlalchemy-spanner): native UUID data type support in dialect
1 parent 788208b commit 681f3de

2 files changed

Lines changed: 104 additions & 0 deletions

File tree

packages/sqlalchemy-spanner/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ def process(value):
123123
"TOKENLIST": types.String,
124124
}
125125

126+
if hasattr(types, "UUID"):
127+
_type_map["UUID"] = types.UUID
128+
126129

127130
_type_map_inv = {
128131
types.Boolean: "BOOL",
@@ -141,6 +144,12 @@ def process(value):
141144
types.NullType: "INT64",
142145
}
143146

147+
if hasattr(types, "UUID"):
148+
_type_map_inv[types.UUID] = "UUID"
149+
150+
if hasattr(types, "Uuid"):
151+
_type_map_inv[types.Uuid] = "UUID"
152+
144153
_compound_keywords = {
145154
selectable.CompoundSelect.UNION: "UNION DISTINCT",
146155
selectable.CompoundSelect.UNION_ALL: "UNION ALL",
@@ -765,6 +774,12 @@ class SpannerTypeCompiler(GenericTypeCompiler):
765774
Maps SQLAlchemy types to Spanner data types.
766775
"""
767776

777+
def visit_uuid(self, type_, **kw):
778+
if not type_.native_uuid or not self.dialect.supports_native_uuid:
779+
return self.visit_CHAR(types.CHAR(36), **kw)
780+
else:
781+
return "UUID"
782+
768783
def visit_INTEGER(self, type_, **kw):
769784
return "INT64"
770785

@@ -847,6 +862,7 @@ class SpannerDialect(DefaultDialect):
847862
supports_identity_columns = True
848863
supports_native_boolean = True
849864
supports_native_decimal = True
865+
supports_native_uuid = True
850866
supports_statement_cache = True
851867
# Spanner uses protos for enums. Creating a column like
852868
# Column("an_enum", Enum("A", "B", "C")) will result in a String
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Copyright 2026 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 sqlalchemy import Column, MetaData, Table, types
16+
from sqlalchemy.schema import CreateTable
17+
from sqlalchemy.testing import eq_, fixtures
18+
from google.cloud.sqlalchemy_spanner.sqlalchemy_spanner import (
19+
SpannerDialect,
20+
_type_map,
21+
_type_map_inv,
22+
)
23+
24+
25+
class UuidTest(fixtures.TestBase):
26+
def test_uuid_type_mapping(self):
27+
"""Test UUID is registered in _type_map and _type_map_inv."""
28+
assert "UUID" in _type_map
29+
eq_(_type_map["UUID"], types.UUID)
30+
assert types.UUID in _type_map_inv
31+
eq_(_type_map_inv[types.UUID], "UUID")
32+
assert types.Uuid in _type_map_inv
33+
eq_(_type_map_inv[types.Uuid], "UUID")
34+
35+
def test_uuid_designate_type(self):
36+
"""Test reflecting UUID type string returns types.UUID."""
37+
dialect = SpannerDialect()
38+
assert dialect.supports_native_uuid is True
39+
col_type = dialect._designate_type("UUID")
40+
eq_(col_type, types.UUID)
41+
42+
def test_uuid_ddl_compilation_default(self):
43+
"""Test DDL compilation emits UUID type by default when supports_native_uuid
44+
is True.
45+
"""
46+
dialect = SpannerDialect()
47+
metadata = MetaData()
48+
table = Table(
49+
"test_uuid_table",
50+
metadata,
51+
Column("user_id", types.Uuid, primary_key=True),
52+
)
53+
statement = str(CreateTable(table).compile(dialect=dialect)).strip()
54+
assert "user_id UUID NOT NULL" in statement
55+
56+
def test_uuid_ddl_compilation_native_disabled(self):
57+
"""Test DDL compilation falls back to STRING(36) when supports_native_uuid
58+
is False.
59+
"""
60+
dialect = SpannerDialect()
61+
dialect.supports_native_uuid = False
62+
metadata = MetaData()
63+
table = Table(
64+
"test_uuid_table",
65+
metadata,
66+
Column("user_id", types.Uuid, primary_key=True),
67+
)
68+
statement = str(CreateTable(table).compile(dialect=dialect)).strip()
69+
assert "user_id STRING(36) NOT NULL" in statement
70+
71+
def test_uuid_python_conversion_legacy(self):
72+
"""Test that types.Uuid automatically converts uuid.UUID to/from str
73+
when native_uuid is False.
74+
"""
75+
import uuid
76+
77+
dialect = SpannerDialect()
78+
dialect.supports_native_uuid = False
79+
uuid_type = types.Uuid()
80+
test_uuid = uuid.UUID("123e4567-e89b-12d3-a456-426614174000")
81+
82+
# Test bind processor converts uuid.UUID -> str
83+
bind_proc = uuid_type.bind_processor(dialect)
84+
eq_(bind_proc(test_uuid), "123e4567e89b12d3a456426614174000")
85+
86+
# Test result processor converts str -> uuid.UUID
87+
res_proc = uuid_type.result_processor(dialect, "STRING")
88+
eq_(res_proc("123e4567-e89b-12d3-a456-426614174000"), test_uuid)

0 commit comments

Comments
 (0)