refactor: use migration utilities and handle duplicates in tag constraint migration
Address review feedback from mistercrunch:
- Replace raw Alembic operations with migration_utils functions
- Add duplicate tag handling in upgrade to prevent constraint errors
- Document downgrade limitation when duplicate names exist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
diff --git a/superset/migrations/versions/2025-10-06_16-05_b54f3bd8e69_update_tag_unique_constraint.py b/superset/migrations/versions/2025-10-06_16-05_b54f3bd8e69_update_tag_unique_constraint.py
index 6cc4356..594142f 100644
--- a/superset/migrations/versions/2025-10-06_16-05_b54f3bd8e69_update_tag_unique_constraint.py
+++ b/superset/migrations/versions/2025-10-06_16-05_b54f3bd8e69_update_tag_unique_constraint.py
@@ -22,41 +22,95 @@
"""
+import enum
+
+import migration_utils as utils
from alembic import op
+from sqlalchemy import Column, Enum, Integer, MetaData, String, Table, Text
+from sqlalchemy.sql import func, select
# revision identifiers, used by Alembic.
revision = "b54f3bd8e69"
down_revision = "c233f5365c9e"
+class TagType(enum.Enum):
+ # pylint: disable=invalid-name
+ custom = 1
+ type = 2
+ owner = 3
+ favorited_by = 4
+
+
+# Define the tag table structure for data operations
+metadata = MetaData()
+tag_table = Table(
+ "tag",
+ metadata,
+ Column("id", Integer, primary_key=True),
+ Column("name", String(250)),
+ Column("type", Enum(TagType)),
+ Column("description", Text),
+)
+
+old_constraint_name = "tag_name_key"
+new_constraint_name = "uix_tag_name_type"
+table_name = "tag"
+new_constraint_columns = ["name", "type"]
+
+
def upgrade():
"""
Change tag unique constraint from name only to (name, type) composite.
This allows the same tag name to exist with different types (e.g., 'type:dashboard'
can be both a system tag with type='type' and a custom tag with type='custom').
"""
- try:
- # Drop the old unique constraint on name only
- op.drop_constraint("tag_name_key", "tag", type_="unique")
+ bind = op.get_bind()
- # Create new composite unique constraint on (name, type)
- op.create_unique_constraint("uix_tag_name_type", "tag", ["name", "type"])
- except Exception: # noqa: S110
- # SQLite doesn't support constraint operations well
- pass
+ # Reflect the current database state to get existing tables
+ metadata.reflect(bind=bind)
+
+ # Delete duplicate tags if any, keeping the one with the lowest ID
+ min_id_subquery = (
+ select(
+ [
+ func.min(tag_table.c.id).label("min_id"),
+ tag_table.c.name,
+ tag_table.c.type,
+ ]
+ )
+ .group_by(
+ tag_table.c.name,
+ tag_table.c.type,
+ )
+ .alias("min_ids")
+ )
+
+ delete_query = tag_table.delete().where(
+ tag_table.c.id.notin_(select([min_id_subquery.c.min_id]))
+ )
+
+ bind.execute(delete_query)
+
+ # Drop the old unique constraint on name only
+ utils.drop_unique_constraint(op, old_constraint_name, table_name)
+
+ # Create new composite unique constraint on (name, type)
+ utils.create_unique_constraint(
+ op, new_constraint_name, table_name, new_constraint_columns
+ )
def downgrade():
"""
Revert to name-only unique constraint.
- Note: This may fail if there are duplicate names with different types.
- """
- try:
- # Drop the composite unique constraint
- op.drop_constraint("uix_tag_name_type", "tag", type_="unique")
- # Recreate the old unique constraint on name only
- op.create_unique_constraint("tag_name_key", "tag", ["name"])
- except Exception: # noqa: S110
- # SQLite doesn't support constraint operations well
- pass
+ WARNING: This downgrade will fail if there are duplicate tag names with
+ different types in the database. Before downgrading, ensure there are no
+ tags with the same name but different types, or manually consolidate them.
+ """
+ # Drop the composite unique constraint
+ utils.drop_unique_constraint(op, new_constraint_name, table_name)
+
+ # Recreate the old unique constraint on name only
+ utils.create_unique_constraint(op, old_constraint_name, table_name, ["name"])