"""Add github_id to identity_persons. Revision ID: 369 Revises: 368 Create Date: 2026-06-30 GitHub username/handle, sourced from SuccessFactors ``cust_Other.cust_id`` (a custom MDF entity keyed by ``externalCode`` = employee_no). Verified live 2026-06-30: the field IS populated for the employees who filled the onboarding 「其他声明」form (handles like ``KennethLeeJE8`` — the prior 2026-06-26 ADR that called it "empty, no source" is now stale). Stored RAW (junk values "无"/"none"/empty skipped at the sync layer); exposed as the ``github_id`` mapping source field and projected to the Feishu ``GitHub ID`` custom attribute (``custom_attrs.github_id``). Nullable, no FK (TiDB-safe), sparse by design — write-when-present, never NULL-out. ``String(128)`` is ample for the ≤39-char GitHub handle (and tolerates the occasional pasted URL). """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "369" down_revision = "368" branch_labels = None depends_on = None _TABLE = "identity_persons" _COL = "github_id" def _has_column(bind, table: str, column: str) -> bool: return bind.execute( sa.text( "SELECT 1 FROM information_schema.COLUMNS " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t " "AND COLUMN_NAME = :c" ), {"t": table, "c": column}, ).scalar() is not None def upgrade() -> None: bind = op.get_bind() if not _has_column(bind, _TABLE, _COL): op.add_column(_TABLE, sa.Column(_COL, sa.String(128), nullable=True)) def downgrade() -> None: bind = op.get_bind() if _has_column(bind, _TABLE, _COL): op.drop_column(_TABLE, _COL)