From 5f1698add6ac2de5a6fbd795ea69344f97d4f1e3 Mon Sep 17 00:00:00 2001 From: fenglin <790872612@qq.com> Date: Wed, 11 Feb 2026 10:22:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20add=20unique=20constraint=20to=20tenant?= =?UTF-8?q?=5Fdefault=5Fmodels=20to=20prevent=20duplic=E2=80=A6=20(#31221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: qiaofenglin Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Novice --- ...3ffe2c8_fix_tenant_default_model_unique.py | 59 +++++++++++++++++++ api/models/provider.py | 1 + 2 files changed, 60 insertions(+) create mode 100644 api/migrations/versions/2026_02_10_1507-f55813ffe2c8_fix_tenant_default_model_unique.py diff --git a/api/migrations/versions/2026_02_10_1507-f55813ffe2c8_fix_tenant_default_model_unique.py b/api/migrations/versions/2026_02_10_1507-f55813ffe2c8_fix_tenant_default_model_unique.py new file mode 100644 index 0000000000..f09e086c34 --- /dev/null +++ b/api/migrations/versions/2026_02_10_1507-f55813ffe2c8_fix_tenant_default_model_unique.py @@ -0,0 +1,59 @@ +"""add unique constraint to tenant_default_models + +Revision ID: fix_tenant_default_model_unique +Revises: 9d77545f524e +Create Date: 2026-01-19 15:07:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + + +def _is_pg(conn): + return conn.dialect.name == "postgresql" + + +# revision identifiers, used by Alembic. +revision = 'f55813ffe2c8' +down_revision = 'c3df22613c99' +branch_labels = None +depends_on = None + + +def upgrade(): + # First, remove duplicate records keeping only the most recent one per (tenant_id, model_type) + # This is necessary before adding the unique constraint + conn = op.get_bind() + + # Delete duplicates: keep the record with the latest updated_at for each (tenant_id, model_type) + # If updated_at is the same, keep the one with the largest id as tiebreaker + if _is_pg(conn): + # PostgreSQL: Use DISTINCT ON for efficient deduplication + conn.execute(sa.text(""" + DELETE FROM tenant_default_models + WHERE id NOT IN ( + SELECT DISTINCT ON (tenant_id, model_type) id + FROM tenant_default_models + ORDER BY tenant_id, model_type, updated_at DESC, id DESC + ) + """)) + else: + # MySQL: Use self-join to find and delete duplicates + # Keep the record with latest updated_at (or largest id if updated_at is equal) + conn.execute(sa.text(""" + DELETE t1 FROM tenant_default_models t1 + INNER JOIN tenant_default_models t2 + ON t1.tenant_id = t2.tenant_id + AND t1.model_type = t2.model_type + AND (t1.updated_at < t2.updated_at + OR (t1.updated_at = t2.updated_at AND t1.id < t2.id)) + """)) + + # Now add the unique constraint + with op.batch_alter_table('tenant_default_models', schema=None) as batch_op: + batch_op.create_unique_constraint('unique_tenant_default_model_type', ['tenant_id', 'model_type']) + + +def downgrade(): + with op.batch_alter_table('tenant_default_models', schema=None) as batch_op: + batch_op.drop_constraint('unique_tenant_default_model_type', type_='unique') diff --git a/api/models/provider.py b/api/models/provider.py index 441b54c797..6175a3ae88 100644 --- a/api/models/provider.py +++ b/api/models/provider.py @@ -181,6 +181,7 @@ class TenantDefaultModel(TypeBase): __table_args__ = ( sa.PrimaryKeyConstraint("id", name="tenant_default_model_pkey"), sa.Index("tenant_default_model_tenant_id_provider_type_idx", "tenant_id", "provider_name", "model_type"), + sa.UniqueConstraint("tenant_id", "model_type", name="unique_tenant_default_model_type"), ) id: Mapped[str] = mapped_column(