"""Separate project identity from source checkout configuration.""" from collections.abc import Sequence from urllib.parse import urlsplit import sqlalchemy as sa from alembic import op revision: str = "20260804_0007" down_revision: str | Sequence[str] | None = "20260804_0006" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def _legacy_clone_url(value: str) -> str: parsed = urlsplit(value) if parsed.scheme == "git" and parsed.hostname and parsed.path: port = f":{parsed.port}" if parsed.port else "" return f"https://{parsed.hostname}{port}{parsed.path}.git" return value def upgrade() -> None: with op.batch_alter_table("projects") as batch: batch.add_column(sa.Column("git_remote_normalized", sa.Text())) batch.add_column( sa.Column("curation_enabled", sa.Boolean(), nullable=False, server_default=sa.true()) ) batch.add_column( sa.Column("source_strategy", sa.String(20), nullable=False, server_default="auto") ) batch.add_column(sa.Column("source_branch", sa.String(200))) batch.add_column(sa.Column("source_credential_item_id", sa.String(100))) batch.add_column(sa.Column("source_last_commit", sa.String(64))) batch.add_column(sa.Column("source_last_branch", sa.String(200))) batch.add_column(sa.Column("source_last_fetched_at", sa.DateTime(timezone=True))) batch.add_column(sa.Column("source_last_error", sa.Text())) batch.add_column( sa.Column("source_cache_used", sa.Boolean(), nullable=False, server_default=sa.false()) ) batch.add_column(sa.Column("last_agent_sync_at", sa.DateTime(timezone=True))) batch.add_column(sa.Column("last_agent_branch", sa.String(200))) batch.add_column(sa.Column("last_agent_commit", sa.String(64))) batch.add_column(sa.Column("last_agent_dirty", sa.Boolean())) connection = op.get_bind() projects = connection.execute(sa.text("SELECT id, git_remote FROM projects")).mappings() for project in projects: remote = project["git_remote"] if not remote: continue connection.execute( sa.text( "UPDATE projects " "SET git_remote = :clone_url, git_remote_normalized = :normalized " "WHERE id = :project_id" ), { "clone_url": _legacy_clone_url(remote), "normalized": remote, "project_id": project["id"], }, ) with op.batch_alter_table("projects") as batch: batch.create_unique_constraint( "uq_projects_git_remote_normalized", ["git_remote_normalized"] ) batch.create_index("ix_projects_curation_enabled", ["curation_enabled"]) def downgrade() -> None: connection = op.get_bind() connection.execute( sa.text( "UPDATE projects SET git_remote = git_remote_normalized " "WHERE git_remote_normalized IS NOT NULL" ) ) with op.batch_alter_table("projects") as batch: batch.drop_index("ix_projects_curation_enabled") batch.drop_constraint("uq_projects_git_remote_normalized", type_="unique") batch.drop_column("last_agent_dirty") batch.drop_column("last_agent_commit") batch.drop_column("last_agent_branch") batch.drop_column("last_agent_sync_at") batch.drop_column("source_cache_used") batch.drop_column("source_last_error") batch.drop_column("source_last_fetched_at") batch.drop_column("source_last_branch") batch.drop_column("source_last_commit") batch.drop_column("source_credential_item_id") batch.drop_column("source_branch") batch.drop_column("source_strategy") batch.drop_column("curation_enabled") batch.drop_column("git_remote_normalized")