"""Intern attendance: make ``work_days`` duration-weighted (Integer→Float). 出勤天数 for interns is now counted by clocked duration, not whole days: a day on the clock accrues ``min(1.0, span/9h)`` (满勤标准 = 每日打卡总时长满 9 小时含午休), rounded to 0.1 天. A short day like 15:15→18:38 (~3.4h) now counts as 0.4 天 instead of a full day. The denormalised ``attendance_reports.work_days`` column must therefore hold a fractional value. Idempotent + safe: existing integer values (historical, already-confirmed reports) survive the widening unchanged — 19 stays 19.0. Only cycles re-generated after this ships recompute with the fractional logic (which is correct: a confirmed historical report should not silently change). """ import sqlalchemy as sa from alembic import op revision = "403" down_revision = "402" branch_labels = None depends_on = None def _column_type(bind, table: str, column: str) -> str | None: return bind.execute( sa.text( "SELECT DATA_TYPE FROM information_schema.COLUMNS " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t " "AND COLUMN_NAME = :c" ), {"t": table, "c": column}, ).scalar() def upgrade() -> None: bind = op.get_bind() dtype = (_column_type(bind, "attendance_reports", "work_days") or "").lower() # Only alter if it isn't already a floating type (re-run safe). if dtype in ("float", "double", "decimal", "newdecimal"): return op.alter_column( "attendance_reports", "work_days", existing_type=sa.Integer(), type_=sa.Float(), existing_nullable=False, existing_server_default=None, ) def downgrade() -> None: bind = op.get_bind() dtype = (_column_type(bind, "attendance_reports", "work_days") or "").lower() if dtype == "int": return # Fractional values round to nearest whole day on the way down. op.execute(sa.text("UPDATE attendance_reports SET work_days = ROUND(work_days)")) op.alter_column( "attendance_reports", "work_days", existing_type=sa.Float(), type_=sa.Integer(), existing_nullable=False, )