84 lines
2.3 KiB
Python
84 lines
2.3 KiB
Python
"""add project create idempotency indexes
|
|
|
|
Revision ID: add_project_create_idempotency_indexes
|
|
Revises: add_work_item_draft_confirmation_fields
|
|
Create Date: 2026-03-24
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
revision = "add_project_create_idempotency_indexes"
|
|
down_revision = "add_work_item_draft_confirmation_fields"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def _table_exists(bind, table_name: str) -> bool:
|
|
return sa.inspect(bind).has_table(table_name)
|
|
|
|
|
|
def _duplicate_active_titles(bind) -> list[tuple[int, str, int]]:
|
|
rows = bind.execute(
|
|
sa.text(
|
|
"""
|
|
SELECT
|
|
tenant_id,
|
|
lower(coalesce(nullif(title, ''), nullif(name, ''))) AS normalized_title,
|
|
count(*) AS duplicate_count
|
|
FROM projects
|
|
WHERE status <> 'inactive'
|
|
AND coalesce(nullif(title, ''), nullif(name, '')) IS NOT NULL
|
|
GROUP BY tenant_id, lower(coalesce(nullif(title, ''), nullif(name, '')))
|
|
HAVING count(*) > 1
|
|
ORDER BY duplicate_count DESC, tenant_id ASC
|
|
LIMIT 10
|
|
"""
|
|
)
|
|
).fetchall()
|
|
return [
|
|
(int(row[0]), str(row[1] or ""), int(row[2]))
|
|
for row in rows
|
|
]
|
|
|
|
|
|
def upgrade() -> None:
|
|
bind = op.get_bind()
|
|
if not _table_exists(bind, "projects"):
|
|
return
|
|
|
|
duplicates = _duplicate_active_titles(bind)
|
|
if duplicates:
|
|
sample = ", ".join(
|
|
f"tenant={tenant_id}, title={title}, count={count}"
|
|
for tenant_id, title, count in duplicates[:3]
|
|
)
|
|
raise RuntimeError(
|
|
"cannot create uq_projects_tenant_active_title_ci because duplicate active project titles exist: "
|
|
+ sample
|
|
)
|
|
|
|
op.execute(
|
|
sa.text(
|
|
"""
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_projects_tenant_active_title_ci
|
|
ON projects (
|
|
tenant_id,
|
|
lower(coalesce(nullif(title, ''), nullif(name, '')))
|
|
)
|
|
WHERE status <> 'inactive'
|
|
AND coalesce(nullif(title, ''), nullif(name, '')) IS NOT NULL
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
bind = op.get_bind()
|
|
if not _table_exists(bind, "projects"):
|
|
return
|
|
op.execute(sa.text("DROP INDEX IF EXISTS uq_projects_tenant_active_title_ci"))
|