]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
mssql: fall back to base type for alias types during reflection
authorCarlos Serrano <caseov@gmail.com>
Wed, 18 Mar 2026 15:37:08 +0000 (11:37 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 20 Mar 2026 19:29:08 +0000 (15:29 -0400)
Fixed regression from version 2.0.42 caused by :ticket:`12654` where the
updated column reflection query would receive SQL Server "type alias" names
for special types such as ``sysname``, whereas previously the base name
would be received (e.g. ``nvarchar`` for ``sysname``), leading to warnings
that such types could not be reflected and resulting in :class:`.NullType`,
rather than the expected :class:`.NVARCHAR` for a type like ``sysname``.
The column reflection query now joins ``sys.types`` a second time to look
up the base type when the user type name is not present in
:attr:`.MSDialect.ischema_names`, and both names are checked in
:attr:`.MSDialect.ischema_names` for a match. Pull request courtesy Carlos
Serrano.

Fixes: #13181
Fixes: #13182
Closes: #13178
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/13178
Pull-request-sha: be5c3850594665c0154ae215d4f9c322cc5a3f5a

Change-Id: I7fe86b80dfa45b208f7d97003ee5b1df3f07bfe7

doc/build/changelog/unreleased_20/13181.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mssql/base.py
test/dialect/mssql/test_reflection.py

diff --git a/doc/build/changelog/unreleased_20/13181.rst b/doc/build/changelog/unreleased_20/13181.rst
new file mode 100644 (file)
index 0000000..9f0d10a
--- /dev/null
@@ -0,0 +1,15 @@
+.. change::
+    :tags: bug, mssql, reflection
+    :tickets: 13181, 13182
+
+    Fixed regression from version 2.0.42 caused by :ticket:`12654` where the
+    updated column reflection query would receive SQL Server "type alias" names
+    for special types such as ``sysname``, whereas previously the base name
+    would be received (e.g. ``nvarchar`` for ``sysname``), leading to warnings
+    that such types could not be reflected and resulting in :class:`.NullType`,
+    rather than the expected :class:`.NVARCHAR` for a type like ``sysname``.
+    The column reflection query now joins ``sys.types`` a second time to look
+    up the base type when the user type name is not present in
+    :attr:`.MSDialect.ischema_names`, and both names are checked in
+    :attr:`.MSDialect.ischema_names` for a match. Pull request courtesy Carlos
+    Serrano.
index eae3fe7058ae74e73dc89aa8080abac9ff7802d4..722a7a0955c49d551d0ea13c89a47453e7f36bf4 100644 (file)
@@ -3693,6 +3693,7 @@ order by
     def get_columns(self, connection, tablename, dbname, owner, schema, **kw):
         sys_columns = ischema.sys_columns
         sys_types = ischema.sys_types
+        sys_base_types = ischema.sys_types.alias("base_types")
         sys_default_constraints = ischema.sys_default_constraints
         computed_cols = ischema.computed_columns
         identity_cols = ischema.identity_columns
@@ -3734,6 +3735,7 @@ order by
             sql.select(
                 sys_columns.c.name,
                 sys_types.c.name,
+                sys_base_types.c.name.label("base_type"),
                 sys_columns.c.is_nullable,
                 sys_columns.c.max_length,
                 sys_columns.c.precision,
@@ -3753,6 +3755,15 @@ order by
                 onclause=sys_columns.c.user_type_id
                 == sys_types.c.user_type_id,
             )
+            .outerjoin(
+                sys_base_types,
+                onclause=sql.and_(
+                    sys_types.c.system_type_id
+                    == sys_base_types.c.system_type_id,
+                    sys_base_types.c.user_type_id
+                    == sys_base_types.c.system_type_id,
+                ),
+            )
             .outerjoin(
                 sys_default_constraints,
                 sql.and_(
@@ -3799,6 +3810,7 @@ order by
         for row in c.mappings():
             name = row[sys_columns.c.name]
             type_ = row[sys_types.c.name]
+            base_type = row["base_type"]
             nullable = row[sys_columns.c.is_nullable] == 1
             maxlen = row[sys_columns.c.max_length]
             numericprec = row[sys_columns.c.precision]
@@ -3812,7 +3824,17 @@ order by
             identity_increment = row[identity_cols.c.increment_value]
             comment = row[extended_properties.c.value]
 
+            # Try to resolve the user type first (e.g., "sysname"),
+            # then fall back to the base type (e.g., "nvarchar").
+            # base_type may be None for CLR types (geography, geometry,
+            # hierarchyid) which have no corresponding base type.
             coltype = self.ischema_names.get(type_, None)
+            if (
+                coltype is None
+                and base_type is not None
+                and base_type != type_
+            ):
+                coltype = self.ischema_names.get(base_type, None)
 
             kwargs = {}
 
@@ -3840,10 +3862,16 @@ order by
                     kwargs["collation"] = collation
 
             if coltype is None:
-                util.warn(
-                    "Did not recognize type '%s' of column '%s'"
-                    % (type_, name)
-                )
+                if base_type is not None and base_type != type_:
+                    util.warn(
+                        "Did not recognize type '%s' (user type) or '%s' "
+                        "(base type) of column '%s'" % (type_, base_type, name)
+                    )
+                else:
+                    util.warn(
+                        "Did not recognize type '%s' of column '%s'"
+                        % (type_, name)
+                    )
                 coltype = sqltypes.NULLTYPE
             else:
                 if issubclass(coltype, sqltypes.NumericCommon):
index 4569a5249fc028b670616d8f1f7730f367145b84..0c9f1485a07eaeeff327ba16def8e01f812ff964 100644 (file)
@@ -1352,3 +1352,45 @@ class IdentityReflectionTest(fixtures.TablesTest):
             is_true("dialect_options" not in col)
             is_true("identity" in col)
             eq_(col["identity"], {})
+
+
+class AliasTypeReflectionTest(fixtures.TestBase):
+    """Test reflection of alias and CLR types via base-type fallback.
+
+    issue #13181
+
+    SYSNAME is a built-in alias for NVARCHAR(128); geography, geometry,
+    and hierarchyid are CLR types with no base system type row.
+    """
+
+    __only_on__ = "mssql"
+    __backend__ = True
+
+    def test_sysname_column_reflects_as_nvarchar(self, metadata, connection):
+        connection.exec_driver_sql(
+            "create table sysname_t (id integer primary key, name sysname)"
+        )
+
+        insp = inspect(connection)
+        cols = {c["name"]: c for c in insp.get_columns("sysname_t")}
+        assert isinstance(cols["name"]["type"], mssql.NVARCHAR)
+        assert not isinstance(cols["name"]["type"], sqltypes.NullType)
+
+    @testing.combinations(
+        "geography",
+        "geometry",
+        "hierarchyid",
+        argnames="type_name",
+    )
+    def test_clr_type_reflection(self, metadata, connection, type_name):
+        connection.exec_driver_sql(
+            "create table clr_t (id integer primary key, "
+            "data %s)" % type_name
+        )
+
+        with testing.expect_warnings(
+            "Did not recognize type '%s' of column 'data'" % type_name
+        ):
+            insp = inspect(connection)
+            cols = {c["name"]: c for c in insp.get_columns("clr_t")}
+        assert isinstance(cols["data"]["type"], sqltypes.NullType)