3eb11b1745
Audit (docs/migration_history_audit.md): - files.size_bytes: INTEGER (Alembic) vs BIGINT (Produktion/Plugin) - GIN-Indizes: Fehlendes USING GIN in Alembic 0002 - guest_users: ix_guest_users_email_tenant fehlt UNIQUE in Alembic 0059 - plugins.name: Doppelter Unique-Index in Produktion Forward-Migrationen: - 0093: files.size_bytes INTEGER → BIGINT - 0094: GIN-Indizes reparieren + plugins.name doppelten Index entfernen - 0095: guest_users email+tenant_id UNIQUE INDEX (mit Dubletten-Check) - 0096: Workspace tenant_integrity (tenant-bound FKs) Tests: 41/41 bestanden (17 Workspace + 24 Command) Alembic Head: 0096
55 lines
1.6 KiB
Python
55 lines
1.6 KiB
Python
"""Fix guest_users email+tenant_id unique index.
|
|
|
|
Alembic 0059 created ix_guest_users_email_tenant as a normal (non-unique) index.
|
|
The SQLAlchemy model defines it as unique=True, and production already has
|
|
a UNIQUE INDEX. This migration aligns Alembic with production.
|
|
|
|
Before creating the unique index, checks for duplicate (email, tenant_id) pairs.
|
|
If duplicates exist, the migration aborts with a data cleanup report.
|
|
|
|
Revision ID: 0095
|
|
Revises: 0094
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
revision = "0095"
|
|
down_revision = "0094"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# Check for duplicates before creating unique index
|
|
conn = op.get_bind()
|
|
duplicates = conn.execute(
|
|
sa.text(
|
|
"SELECT email, tenant_id, count(*) FROM guest_users "
|
|
"GROUP BY email, tenant_id HAVING count(*) > 1"
|
|
)
|
|
).fetchall()
|
|
|
|
if duplicates:
|
|
raise RuntimeError(
|
|
f"Cannot create unique index: {len(duplicates)} duplicate (email, tenant_id) pairs found. "
|
|
"Data cleanup required before migration."
|
|
)
|
|
|
|
# Drop the non-unique index and recreate as unique
|
|
op.execute("DROP INDEX IF EXISTS ix_guest_users_email_tenant")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS ix_guest_users_email_tenant "
|
|
"ON guest_users (email, tenant_id)"
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.execute("DROP INDEX IF EXISTS ix_guest_users_email_tenant")
|
|
op.execute(
|
|
"CREATE INDEX IF NOT EXISTS ix_guest_users_email_tenant "
|
|
"ON guest_users (email, tenant_id)"
|
|
)
|