"""add phone auth identity Revision ID: 20260428_add_phone_auth_identity Revises: 20260428_repair_admin_yangyu_workspace_state Create Date: 2026-04-28 """ from __future__ import annotations from alembic import op import sqlalchemy as sa revision = "20260428_add_phone_auth_identity" down_revision = "20260428_repair_admin_yangyu_workspace_state" branch_labels = None depends_on = None def _table_exists(bind, table_name: str) -> bool: return sa.inspect(bind).has_table(table_name) def _column_exists(bind, table_name: str, column_name: str) -> bool: if not _table_exists(bind, table_name): return False return any(column["name"] == column_name for column in sa.inspect(bind).get_columns(table_name)) def _index_exists(bind, table_name: str, index_name: str) -> bool: if not _table_exists(bind, table_name): return False return any(index["name"] == index_name for index in sa.inspect(bind).get_indexes(table_name)) def upgrade() -> None: bind = op.get_bind() if _table_exists(bind, "users"): for column in ( sa.Column("phone_country_code", sa.String(length=8), nullable=True), sa.Column("phone_number", sa.String(length=32), nullable=True), sa.Column("phone_e164", sa.String(length=32), nullable=True), sa.Column("phone_verified_at", sa.TIMESTAMP(), nullable=True), sa.Column("last_login_at", sa.TIMESTAMP(), nullable=True), sa.Column("login_method", sa.String(length=32), nullable=True), ): if not _column_exists(bind, "users", column.name): op.add_column("users", column) if not _index_exists(bind, "users", "ix_users_phone_e164"): op.create_index("ix_users_phone_e164", "users", ["phone_e164"], unique=True) if _table_exists(bind, "tenant_invitations"): for column in ( sa.Column("phone_country_code", sa.String(length=8), nullable=True), sa.Column("phone_number", sa.String(length=32), nullable=True), sa.Column("phone_e164", sa.String(length=32), nullable=True), ): if not _column_exists(bind, "tenant_invitations", column.name): op.add_column("tenant_invitations", column) if not _index_exists(bind, "tenant_invitations", "ix_tenant_invitations_phone_e164"): op.create_index("ix_tenant_invitations_phone_e164", "tenant_invitations", ["phone_e164"], unique=False) if bind.dialect.name == "postgresql" and not _index_exists( bind, "tenant_invitations", "uq_tenant_invitations_pending_phone", ): op.execute( """ CREATE UNIQUE INDEX uq_tenant_invitations_pending_phone ON tenant_invitations (tenant_id, phone_e164) WHERE status = 'pending' AND phone_e164 IS NOT NULL """ ) if not _table_exists(bind, "phone_verification_codes"): op.create_table( "phone_verification_codes", sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.TIMESTAMP(), server_default=sa.func.now(), nullable=False), sa.Column("phone_e164", sa.String(length=32), nullable=False, index=True), sa.Column("purpose", sa.String(length=32), nullable=False, index=True), sa.Column("code_hash", sa.String(length=128), nullable=False), sa.Column("expires_at", sa.TIMESTAMP(), nullable=False), sa.Column("consumed_at", sa.TIMESTAMP(), nullable=True), sa.Column("attempt_count", sa.Integer(), nullable=False, server_default="0"), sa.Column("request_ip", sa.String(length=64), nullable=True), ) op.create_index( "ix_phone_verification_codes_phone_purpose_created", "phone_verification_codes", ["phone_e164", "purpose", "created_at"], unique=False, ) def downgrade() -> None: bind = op.get_bind() if _table_exists(bind, "phone_verification_codes"): if _index_exists(bind, "phone_verification_codes", "ix_phone_verification_codes_phone_purpose_created"): op.drop_index("ix_phone_verification_codes_phone_purpose_created", table_name="phone_verification_codes") op.drop_table("phone_verification_codes") if _table_exists(bind, "tenant_invitations"): if bind.dialect.name == "postgresql" and _index_exists(bind, "tenant_invitations", "uq_tenant_invitations_pending_phone"): op.drop_index("uq_tenant_invitations_pending_phone", table_name="tenant_invitations") if _index_exists(bind, "tenant_invitations", "ix_tenant_invitations_phone_e164"): op.drop_index("ix_tenant_invitations_phone_e164", table_name="tenant_invitations") for column_name in ("phone_e164", "phone_number", "phone_country_code"): if _column_exists(bind, "tenant_invitations", column_name): op.drop_column("tenant_invitations", column_name) if _table_exists(bind, "users"): if _index_exists(bind, "users", "ix_users_phone_e164"): op.drop_index("ix_users_phone_e164", table_name="users") for column_name in ( "login_method", "last_login_at", "phone_verified_at", "phone_e164", "phone_number", "phone_country_code", ): if _column_exists(bind, "users", column_name): op.drop_column("users", column_name)