From: Carlos Serrano Date: Wed, 18 Mar 2026 15:37:08 +0000 (-0400) Subject: mssql: fall back to base type for alias types during reflection X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1c78168f8a1a9cd3c88f1977bba1bac763225482;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git mssql: fall back to base type for alias types during reflection 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 --- diff --git a/doc/build/changelog/unreleased_20/13181.rst b/doc/build/changelog/unreleased_20/13181.rst new file mode 100644 index 0000000000..9f0d10a402 --- /dev/null +++ b/doc/build/changelog/unreleased_20/13181.rst @@ -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. diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index eae3fe7058..722a7a0955 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -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): diff --git a/test/dialect/mssql/test_reflection.py b/test/dialect/mssql/test_reflection.py index 4569a5249f..0c9f1485a0 100644 --- a/test/dialect/mssql/test_reflection.py +++ b/test/dialect/mssql/test_reflection.py @@ -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)