From: Mike Bayer Date: Wed, 12 Aug 2026 19:30:42 +0000 (-0400) Subject: Don't apply a length to reflected TEXT / NTEXT X-Git-Url: http://git.ipfire.org/index.cgi?a=commitdiff_plain;h=325f71701a8a994c982c781f59bf34f42add88e7;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Don't apply a length to reflected TEXT / NTEXT Fixed issue in SQL Server reflection where ``TEXT`` and ``NTEXT`` columns would be reflected with a spurious length of 16 and 8, respectively. These are unlengthed LOB datatypes; the value originates from the ``sys.columns.max_length`` column, which reports the size of the in-row LOB pointer rather than a character length for these types. The reflected ``TEXT`` and ``NTEXT`` types now have a ``length`` of ``None``, so that a reflected table emits valid DDL when re-created, which previously failed with "Cannot specify a column width on data type text". Fixes: #13451 Change-Id: I8456688fc8d21fe25f326c6bf0f7b13aa1fc838c --- diff --git a/doc/build/changelog/unreleased_20/13451.rst b/doc/build/changelog/unreleased_20/13451.rst new file mode 100644 index 0000000000..fa0502d221 --- /dev/null +++ b/doc/build/changelog/unreleased_20/13451.rst @@ -0,0 +1,13 @@ +.. change:: + :tags: bug, mssql, reflection + :tickets: 13451 + + Fixed issue in SQL Server reflection where ``TEXT`` and ``NTEXT`` columns + would be reflected with a spurious length of 16 and 8, respectively. These + are unlengthed LOB datatypes; the value originates from the + ``sys.columns.max_length`` column, which reports the size of the in-row LOB + pointer rather than a character length for these types. The reflected + :class:`_mssql.TEXT` and :class:`_mssql.NTEXT` types now have a ``length`` + of ``None``, so that a reflected table emits valid DDL when re-created, + which previously failed with "Cannot specify a column width on data type + text". diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 647ee36b60..478109914c 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -3589,14 +3589,20 @@ class MSDialect(default._BackendsMultiReflection, default.DefaultDialect): if coltype in (MSBinary, MSVarBinary, sqltypes.LargeBinary): kwargs["length"] = maxlen if maxlen != -1 else None - elif coltype in (MSString, MSChar, MSText): + elif coltype in (MSString, MSChar): kwargs["length"] = maxlen if maxlen != -1 else None if collation: kwargs["collation"] = collation - elif coltype in (MSNVarchar, MSNChar, MSNText): + elif coltype in (MSNVarchar, MSNChar): kwargs["length"] = maxlen // 2 if maxlen != -1 else None if collation: kwargs["collation"] = collation + elif coltype in (MSText, MSNText): + # TEXT / NTEXT are unlengthed LOB types. sys.columns.max_length + # reports 16 for these, which is the size of the in-row LOB + # pointer and not a character length, so no length is applied. + if collation: + kwargs["collation"] = collation if coltype is None: if base_type is not None and base_type != type_: diff --git a/test/dialect/mssql/test_reflection.py b/test/dialect/mssql/test_reflection.py index 59f5245e72..60ca27e226 100644 --- a/test/dialect/mssql/test_reflection.py +++ b/test/dialect/mssql/test_reflection.py @@ -117,6 +117,37 @@ class ReflectionTest(fixtures.TestBase, ComparesTables, AssertsCompiledSQL): "CREATE TABLE type_test (col1 %s NULL)" % ddl, ) + def test_lob_types_no_length(self, metadata, connection): + """TEXT / NTEXT / IMAGE are unlengthed, and a reflected version of + such a table must remain creatable. + + issue #13451 + + """ + Table( + "lob_type_test", + metadata, + Column("id", types.Integer, primary_key=True), + Column("t", mssql.TEXT), + Column("nt", mssql.NTEXT), + Column("img", mssql.IMAGE), + ) + metadata.create_all(connection) + + m2 = MetaData() + table2 = Table("lob_type_test", m2, autoload_with=connection) + eq_( + {c.name: c.type.length for c in table2.c if c.name != "id"}, + {"t": None, "nt": None, "img": None}, + ) + + # the reflected types round trip back into valid DDL; a length + # here would be rejected with "Cannot specify a column width on + # data type text" + Table( + "lob_type_test_2", metadata, *[c._copy() for c in table2.c] + ).create(connection) + def test_identity(self, metadata, connection): table = Table( "identity_test", @@ -1263,6 +1294,74 @@ class ReflectionTest(fixtures.TestBase, ComparesTables, AssertsCompiledSQL): ) +class ParseColumnInfoTest(fixtures.TestBase): + """test translation of ``sys.columns.max_length`` into type lengths. + + issue #13451 + + """ + + @testing.combinations( + ("varchar", 30, "VARCHAR(30)", 30), + ("varchar", -1, "VARCHAR(max)", None), + ("char", 10, "CHAR(10)", 10), + ("nvarchar", 60, "NVARCHAR(30)", 30), + ("nvarchar", -1, "NVARCHAR(max)", None), + ("nchar", 20, "NCHAR(10)", 10), + ("text", 16, "TEXT", None), + ("ntext", 16, "NTEXT", None), + ("image", 16, "IMAGE", None), + ("varbinary", 20, "VARBINARY(20)", 20), + ("varbinary", -1, "VARBINARY(max)", None), + ("binary", 10, "BINARY(10)", 10), + argnames="type_name, max_length, expected_ddl, expected_length", + ) + def test_length_from_max_length( + self, type_name, max_length, expected_ddl, expected_length + ): + """``max_length`` is 16 for the LOB types text, ntext and image, + that being the size of the in-row LOB pointer rather than a + character length. + + """ + dialect = mssql.dialect() + type_ = self._parse_type(dialect, type_name, max_length, None) + eq_( + (type_.compile(dialect=dialect), type_.length), + (expected_ddl, expected_length), + ) + + @testing.combinations("text", "ntext", argnames="type_name") + def test_lob_types_retain_collation(self, type_name): + """dropping the bogus length must not drop the collation.""" + + dialect = mssql.dialect() + type_ = self._parse_type( + dialect, type_name, 16, "Latin1_General_CI_AS" + ) + eq_(type_.collation, "Latin1_General_CI_AS") + + def _parse_type(self, dialect, type_name, max_length, collation): + cdict = dialect._parse_column_info( + name="data", + type_=type_name, + base_type=type_name, + nullable=True, + maxlen=max_length, + numericprec=0, + numericscale=0, + default=None, + collation=collation, + definition=None, + is_persisted=None, + is_identity=None, + identity_start=None, + identity_increment=None, + comment=None, + ) + return cdict["type"] + + class InfoCoerceUnicodeTest(fixtures.TestBase, AssertsCompiledSQL): def test_info_unicode_cast_no_2000(self): dialect = mssql.dialect()