From 74b5a8cf5ab64731681c3d4526304715c866eafd Mon Sep 17 00:00:00 2001 From: libingtong Date: Sat, 8 Aug 2026 10:58:17 +0800 Subject: [PATCH] fix(migrations): make f061 idempotent so fresh deployments can migrate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 001_initial_schema.py builds the schema with Base.metadata.create_all(), so a freshly provisioned database already has enterprise_info.tenant_id — the model declares it. f061 then calls op.add_column() unconditionally and fails: asyncpg.exceptions.DuplicateColumnError: column "tenant_id" of relation "enterprise_info" already exists `alembic upgrade head` therefore cannot complete on any fresh deployment, and backend/entrypoint.sh aborts before starting uvicorn (it runs migrations first). Reproduce against an empty database: docker run -d --name pg -e POSTGRES_USER=clawith -e POSTGRES_PASSWORD=clawith \ -e POSTGRES_DB=clawith -p 5432:5432 postgres:15-alpine cd backend DATABASE_URL=postgresql+asyncpg://clawith:clawith@127.0.0.1:5432/clawith \ alembic upgrade head Guard the DDL with IF NOT EXISTS / IF EXISTS, matching the convention already used in 010_column_modify.py. This also makes the module docstring's stated "Idempotence: Safe for retry" property actually hold. Existing deployments that predate the column are unaffected — the column is still created there. Verified both paths: fresh database, and an existing database at allow_checkpoint_deliveries upgrading through f060 to f061. Drops two imports that are no longer referenced. --- .../v1_0_0_f061_enterprise_info_tenant_id.py | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/backend/alembic/versions/v1_0_0_f061_enterprise_info_tenant_id.py b/backend/alembic/versions/v1_0_0_f061_enterprise_info_tenant_id.py index e2f2b48fe..0ca62ff85 100644 --- a/backend/alembic/versions/v1_0_0_f061_enterprise_info_tenant_id.py +++ b/backend/alembic/versions/v1_0_0_f061_enterprise_info_tenant_id.py @@ -21,8 +21,6 @@ from typing import Sequence, Union from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision: str = "f061_enterprise_info_tenant_id" @@ -32,15 +30,38 @@ def upgrade() -> None: + # This migration must be idempotent with respect to a freshly created schema. + # 001_initial_schema.py builds the schema with Base.metadata.create_all(), which + # already produces enterprise_info.tenant_id because the model declares it, so an + # unconditional op.add_column() fails on every fresh deployment and + # `alembic upgrade head` cannot complete. + # + # The IF NOT EXISTS / IF EXISTS guards match the convention already used in + # 010_column_modify.py, and make the module docstring's "Safe for retry" claim hold. + # 1. Add tenant_id column with default uuid generator or nullable first if populated - op.add_column("enterprise_info", sa.Column("tenant_id", postgresql.UUID(as_uuid=True), nullable=True)) - op.create_index(op.f("ix_enterprise_info_tenant_id"), "enterprise_info", ["tenant_id"], unique=False) + op.execute("ALTER TABLE enterprise_info ADD COLUMN IF NOT EXISTS tenant_id UUID") + op.execute( + "CREATE INDEX IF NOT EXISTS ix_enterprise_info_tenant_id ON enterprise_info (tenant_id)" + ) # 2. Drop legacy single info_type unique constraint - op.drop_constraint("enterprise_info_info_type_key", "enterprise_info", type_="unique") + op.execute("ALTER TABLE enterprise_info DROP CONSTRAINT IF EXISTS enterprise_info_info_type_key") # 3. Create new composite unique constraint (tenant_id, info_type) - op.create_unique_constraint("uq_enterprise_info_tenant_type", "enterprise_info", ["tenant_id", "info_type"]) + op.execute( + """ + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'uq_enterprise_info_tenant_type' + ) THEN + ALTER TABLE enterprise_info + ADD CONSTRAINT uq_enterprise_info_tenant_type UNIQUE (tenant_id, info_type); + END IF; + END $$; + """ + ) def downgrade() -> None: