]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
Fix MySQL native ENUM autogenerate detection
authorFurkan Köykıran <furkankoykiran@users.noreply.github.com>
Fri, 6 Mar 2026 15:13:49 +0000 (10:13 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 6 Mar 2026 16:27:13 +0000 (11:27 -0500)
Implemented type comparison for :class:`.ENUM` datatypes on MySQL, which
checks that the individual enum values are equivalent.  If additional
entries are on either side, this generates a diff.  Changes of order do not
generate a diff.  Pull request courtesy Furkan Köykıran.

Fixes: #779
Fixes: #1745
Closes: #1746
Pull-request: https://github.com/sqlalchemy/alembic/pull/1746
Pull-request-sha: 49b17845d89e5938c1616bb77fbb6294d8c4db2f

Change-Id: I0023b659acec603d39feaea45413637fe559ea56

alembic/ddl/mysql.py
docs/build/unreleased/1745.rst [new file with mode: 0644]
tests/test_mysql.py

index 27f808b050541edba840dac6227e7653d6f7de51..85baaaf9436b3b01115e03c68c4b27f0a6f04a6c 100644 (file)
@@ -350,6 +350,41 @@ class MySQLImpl(DefaultImpl):
             ):
                 cnfk.onupdate = "RESTRICT"
 
+    def compare_type(
+        self,
+        inspector_column: schema.Column[Any],
+        metadata_column: schema.Column,
+    ) -> bool:
+        """Override compare_type to properly detect MySQL native ENUM changes.
+
+        This addresses the issue where autogenerate fails to detect when new
+        values are added to or removed from MySQL native ENUM columns.
+        """
+        metadata_type = metadata_column.type
+        inspector_type = inspector_column.type
+
+        # Check if both columns are MySQL native ENUMs
+        if isinstance(metadata_type, sqltypes.Enum) and isinstance(
+            inspector_type, sqltypes.Enum
+        ):
+            metadata_set = set(metadata_type.enums)
+            inspector_set = set(inspector_type.enums)
+            # Compare the actual enum values, ignoring order
+            if metadata_set != inspector_set:
+                return True
+            else:
+                # for MySQL ENUM, there is no other aspect to be compared,
+                # avoid falling into the default compare_type which will
+                # return a false positive for change in order of the enum
+                # elements
+                return False
+
+        # Fall back to default comparison for non-ENUM types
+        # note that this comparison does not work for ENUM values as above
+        # because it considers different lengths of argument lists to be
+        # an "ignore" signal.
+        return super().compare_type(inspector_column, metadata_column)
+
 
 class MariaDBImpl(MySQLImpl):
     __dialect__ = "mariadb"
diff --git a/docs/build/unreleased/1745.rst b/docs/build/unreleased/1745.rst
new file mode 100644 (file)
index 0000000..7d86ba9
--- /dev/null
@@ -0,0 +1,9 @@
+.. change::
+    :tags: bug, mysql
+    :tickets: 779, 1745
+
+    Implemented type comparison for :class:`.ENUM` datatypes on MySQL, which
+    checks that the individual enum values are equivalent.  If additional
+    entries are on either side, this generates a diff.  Changes of order do not
+    generate a diff.  Pull request courtesy Furkan Köykıran.
+
index 399cd34d9942f1af06115e651fa0ed4091acb3a1..97c4d9c030c4bbc2b93bd3dd3416bd5dded0cb3d 100644 (file)
@@ -2,6 +2,7 @@ from sqlalchemy import Boolean
 from sqlalchemy import Column
 from sqlalchemy import Computed
 from sqlalchemy import DATETIME
+from sqlalchemy import Enum
 from sqlalchemy import exc
 from sqlalchemy import Float
 from sqlalchemy import func
@@ -14,10 +15,12 @@ from sqlalchemy import String
 from sqlalchemy import Table
 from sqlalchemy import text
 from sqlalchemy import TIMESTAMP
+from sqlalchemy.dialects.mysql import ENUM as MySQL_ENUM
 from sqlalchemy.dialects.mysql import VARCHAR
 
 from alembic import autogenerate
 from alembic import op
+from alembic import testing
 from alembic import util
 from alembic.autogenerate import api
 from alembic.autogenerate.compare.constraints import _compare_nullable
@@ -27,6 +30,7 @@ from alembic.testing import assert_raises_message
 from alembic.testing import combinations
 from alembic.testing import config
 from alembic.testing import eq_ignore_whitespace
+from alembic.testing import is_
 from alembic.testing.env import clear_staging_env
 from alembic.testing.env import staging_env
 from alembic.testing.fixtures import AlterColRoundTripFixture
@@ -40,6 +44,7 @@ if True:
     from alembic.autogenerate.compare.types import (
         _dialect_impl_compare_type as _compare_type,
     )
+    from alembic.ddl.mysql import MySQLImpl
 
 
 class MySQLOpTest(TestBase):
@@ -784,3 +789,56 @@ class MySQLAutogenRenderTest(TestBase):
             "op.create_index('foo_idx', 't', "
             "['x', sa.literal_column('(coalesce(y, 0))')], unique=False)",
         )
+
+
+class MySQLEnumCompareTest(TestBase):
+    """Test MySQL native ENUM comparison in autogenerate."""
+
+    __only_on__ = "mysql", "mariadb"
+    __backend__ = True
+
+    @testing.fixture()
+    def connection(self):
+        with config.db.begin() as conn:
+            yield conn
+
+    # note False means the two enums are equivalent, True means they
+    # are different
+    @testing.combinations(
+        (
+            Enum("A", "B", "C", native_enum=True),
+            Enum("A", "B", "C", native_enum=True),
+            False,
+        ),
+        (
+            Enum("A", "B", "C", native_enum=True),
+            Enum("A", "B", "C", "D", native_enum=True),
+            True,
+        ),
+        (
+            Enum("A", "B", "C", "D", native_enum=True),
+            Enum("A", "B", "C", native_enum=True),
+            True,
+        ),
+        (
+            Enum("A", "B", "C", native_enum=True),
+            Enum("C", "B", "A", native_enum=True),
+            False,  # These two enums are equivalent, change in order is not
+            # counted
+        ),
+        (MySQL_ENUM("A", "B", "C"), MySQL_ENUM("A", "B", "C"), False),
+        (MySQL_ENUM("A", "B", "C"), MySQL_ENUM("A", "B", "C", "D"), True),
+        id_="ssa",
+        argnames="inspected_type,metadata_type,expected",
+    )
+    def test_compare_enum_types(
+        self, inspected_type, metadata_type, expected, connection
+    ):
+        impl = MySQLImpl(connection.dialect, connection, False, None, None, {})
+
+        is_(
+            impl.compare_type(
+                Column("x", inspected_type), Column("x", metadata_type)
+            ),
+            expected,
+        )