"""Attendance: HR-maintained roster overlay (include / exclude). Adds ``attendance_roster_overrides`` so HR can patch the Feishu-derived intern roster without losing the auto-pull: ``include`` rows drag a person into the report even when they are not in the Feishu 通讯录 department (the 许晴 case — onboarded in Helix but never added to the Feishu group), ``exclude`` rows keep a Feishu member out. Effective roster = (Feishu members ∪ includes) − excludes. ``id`` / ``person_id`` / ``created_by`` use ``id_type_for('identity_persons')`` so the column type matches ``identity_persons.id`` (TiDB is strict about CHAR↔VARCHAR); no DB FK is declared, matching the sibling attendance_cycles / attendance_reports convention (person identity is resolved at the serializer layer). One override per person (UNIQUE person_id). Revision ID: 380 Revises: 379 """ from __future__ import annotations import sqlalchemy as sa from alembic import op from app.core.migration_helpers import id_type_for, table_kw_for revision = "380" down_revision = "379" branch_labels = None depends_on = None TABLE = "attendance_roster_overrides" def _table_exists(bind, name: str) -> bool: return bool(bind.execute( sa.text( "SELECT 1 FROM information_schema.tables " "WHERE table_schema = DATABASE() AND table_name = :t LIMIT 1" ), {"t": name}, ).scalar()) def upgrade() -> None: bind = op.get_bind() if _table_exists(bind, TABLE): return pid = id_type_for("identity_persons") op.create_table( TABLE, sa.Column("id", pid, nullable=False), sa.Column("person_id", pid, nullable=False), sa.Column("mode", sa.String(16), nullable=False), sa.Column("note", sa.Text(), nullable=True), sa.Column("created_by", pid, nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("person_id", name="uq_attendance_roster_overrides_person"), **table_kw_for("identity_persons"), ) def downgrade() -> None: bind = op.get_bind() if _table_exists(bind, TABLE): op.drop_table(TABLE)