fix(backend): Make UserTenantRoles migration idempotent to fix database initialization
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled

Fixed BUG-007 where database migrations failed during initialization because the
user_tenant_roles table was never created by any migration, but a later migration
tried to modify it.

Root Cause:
- The user_tenant_roles table was configured in IdentityDbContext but missing from InitialIdentityModule migration
- Migration 20251103150353_FixUserTenantRolesIgnoreNavigation tried to drop/recreate foreign keys on a non-existent table
- This caused application startup to fail with "relation user_tenant_roles does not exist"

Solution:
- Made the migration idempotent by checking table existence before operations
- If table doesn't exist, create it with proper schema, indexes, and constraints
- Drop foreign keys only if they exist (safe for both first run and re-runs)
- Corrected principal schema references (users/tenants are in default schema at this migration point)
- Removed duplicate ix_user_tenant_roles_tenant_role index (created by later migration)

Testing:
- Clean database initialization:  SUCCESS
- All migrations applied successfully:  SUCCESS
- Application starts and listens:  SUCCESS
- Foreign keys created correctly:  SUCCESS

Impact:
- Fixes P0 CRITICAL bug blocking Docker environment delivery
- Enables clean database initialization from scratch
- Maintains backward compatibility with existing databases

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Yaojia Wang
2025-11-05 09:02:40 +01:00
parent a0e24c2ab7
commit 1413306028

View File

@@ -10,21 +10,68 @@ namespace ColaFlow.Modules.Identity.Infrastructure.Persistence.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
// Drop and recreate foreign keys to ensure they reference the correct columns // IDEMPOTENT FIX: Check if table exists before modifying it
// This fixes BUG-002: Foreign keys were incorrectly referencing user_id1/tenant_id1 // If the table doesn't exist, create it first
migrationBuilder.Sql(@"
-- Create user_tenant_roles table if it doesn't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'identity'
AND table_name = 'user_tenant_roles'
) THEN
-- Create the table
CREATE TABLE identity.user_tenant_roles (
id uuid NOT NULL PRIMARY KEY,
user_id uuid NOT NULL,
tenant_id uuid NOT NULL,
role character varying(50) NOT NULL,
assigned_at timestamp with time zone NOT NULL,
assigned_by_user_id uuid,
CONSTRAINT uq_user_tenant_roles_user_tenant UNIQUE (user_id, tenant_id)
);
migrationBuilder.DropForeignKey( -- Create basic indexes
name: "FK_user_tenant_roles_tenants_tenant_id", -- Note: ix_user_tenant_roles_tenant_role will be created by a later migration
schema: "identity", CREATE INDEX ix_user_tenant_roles_user_id ON identity.user_tenant_roles(user_id);
table: "user_tenant_roles"); CREATE INDEX ix_user_tenant_roles_tenant_id ON identity.user_tenant_roles(tenant_id);
CREATE INDEX ix_user_tenant_roles_role ON identity.user_tenant_roles(role);
END IF;
END $$;
");
migrationBuilder.DropForeignKey( // Drop existing foreign keys if they exist
name: "FK_user_tenant_roles_users_user_id", migrationBuilder.Sql(@"
schema: "identity", DO $$
table: "user_tenant_roles"); BEGIN
-- Drop FK to tenants if it exists
IF EXISTS (
SELECT FROM information_schema.table_constraints
WHERE constraint_schema = 'identity'
AND table_name = 'user_tenant_roles'
AND constraint_name = 'FK_user_tenant_roles_tenants_tenant_id'
) THEN
ALTER TABLE identity.user_tenant_roles
DROP CONSTRAINT ""FK_user_tenant_roles_tenants_tenant_id"";
END IF;
-- Drop FK to users if it exists
IF EXISTS (
SELECT FROM information_schema.table_constraints
WHERE constraint_schema = 'identity'
AND table_name = 'user_tenant_roles'
AND constraint_name = 'FK_user_tenant_roles_users_user_id'
) THEN
ALTER TABLE identity.user_tenant_roles
DROP CONSTRAINT ""FK_user_tenant_roles_users_user_id"";
END IF;
END $$;
");
// Recreate foreign keys with correct column references // Recreate foreign keys with correct column references
// Note: users and tenants tables are in the default schema (no explicit schema) // Note: At this point in time, users and tenants are still in the default schema
// (They will be moved to identity schema in a later migration)
migrationBuilder.AddForeignKey( migrationBuilder.AddForeignKey(
name: "FK_user_tenant_roles_users_user_id", name: "FK_user_tenant_roles_users_user_id",
schema: "identity", schema: "identity",
@@ -47,23 +94,35 @@ namespace ColaFlow.Modules.Identity.Infrastructure.Persistence.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.AddForeignKey( // Drop foreign keys if they exist
name: "FK_user_tenant_roles_tenants_tenant_id", migrationBuilder.Sql(@"
schema: "identity", DO $$
table: "user_tenant_roles", BEGIN
column: "tenant_id", IF EXISTS (
principalTable: "tenants", SELECT FROM information_schema.table_constraints
principalColumn: "id", WHERE constraint_schema = 'identity'
onDelete: ReferentialAction.Cascade); AND table_name = 'user_tenant_roles'
AND constraint_name = 'FK_user_tenant_roles_tenants_tenant_id'
) THEN
ALTER TABLE identity.user_tenant_roles
DROP CONSTRAINT ""FK_user_tenant_roles_tenants_tenant_id"";
END IF;
migrationBuilder.AddForeignKey( IF EXISTS (
name: "FK_user_tenant_roles_users_user_id", SELECT FROM information_schema.table_constraints
schema: "identity", WHERE constraint_schema = 'identity'
table: "user_tenant_roles", AND table_name = 'user_tenant_roles'
column: "user_id", AND constraint_name = 'FK_user_tenant_roles_users_user_id'
principalTable: "users", ) THEN
principalColumn: "id", ALTER TABLE identity.user_tenant_roles
onDelete: ReferentialAction.Cascade); DROP CONSTRAINT ""FK_user_tenant_roles_users_user_id"";
END IF;
END $$;
");
// Note: We don't drop the table in Down() because it should have been created
// by a previous migration. If it was created by this migration (first run),
// then it will be cleaned up when the database is reset.
} }
} }
} }