From c64229ab783484f979a95bfaa2f123ac72798b48 Mon Sep 17 00:00:00 2001 From: souplearn Date: Sat, 25 Jul 2026 02:43:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A0=A1=E9=AA=8C=E6=8A=A5=E5=B7=A5?= =?UTF-8?q?=E5=BD=92=E5=B1=9E=E8=A1=A8=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/migrate_report_allocations.py | 131 +++++++++++++++++++++----- 1 file changed, 110 insertions(+), 21 deletions(-) diff --git a/scripts/migrate_report_allocations.py b/scripts/migrate_report_allocations.py index 69fa36a..b51275e 100644 --- a/scripts/migrate_report_allocations.py +++ b/scripts/migrate_report_allocations.py @@ -13,23 +13,36 @@ from app.database import engine # noqa: E402 TABLE_NAME = "production_report_allocations" IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") REQUIRED_COLUMNS = { - "id", - "report_id", - "report_item_id", - "attendance_point_name", - "employee_phone", - "allocation_date", - "day_minutes", - "overtime_minutes", - "night_minutes", - "effective_minutes", - "good_qty", - "defect_qty", - "scrap_qty", - "changeover_count", - "reference_wage", - "created_at", - "updated_at", + "id": {"column_type": "bigint", "is_nullable": "NO", "extra_contains": ("auto_increment",)}, + "report_id": {"column_type": "bigint", "is_nullable": "NO"}, + "report_item_id": {"column_type": "bigint", "is_nullable": "YES"}, + "attendance_point_name": { + "column_type": "varchar(128)", + "is_nullable": "NO", + "default": "empty_string", + }, + "employee_phone": {"column_type": "varchar(20)", "is_nullable": "NO", "default": "empty_string"}, + "allocation_date": {"column_type": "date", "is_nullable": "NO"}, + "day_minutes": {"column_type": "decimal(10,2)", "is_nullable": "NO", "default": "zero"}, + "overtime_minutes": {"column_type": "decimal(10,2)", "is_nullable": "NO", "default": "zero"}, + "night_minutes": {"column_type": "decimal(10,2)", "is_nullable": "NO", "default": "zero"}, + "effective_minutes": {"column_type": "decimal(10,2)", "is_nullable": "NO", "default": "zero"}, + "good_qty": {"column_type": "decimal(12,2)", "is_nullable": "NO", "default": "zero"}, + "defect_qty": {"column_type": "decimal(12,2)", "is_nullable": "NO", "default": "zero"}, + "scrap_qty": {"column_type": "decimal(12,2)", "is_nullable": "NO", "default": "zero"}, + "changeover_count": {"column_type": "decimal(12,2)", "is_nullable": "NO", "default": "zero"}, + "reference_wage": {"column_type": "decimal(12,2)", "is_nullable": "NO", "default": "zero"}, + "created_at": { + "column_type": "datetime", + "is_nullable": "NO", + "default": "current_timestamp", + }, + "updated_at": { + "column_type": "datetime", + "is_nullable": "NO", + "default": "current_timestamp", + "extra_contains": ("on update current_timestamp",), + }, } REQUIRED_INDEXES = { "idx_report_allocations_report": ("report_id",), @@ -66,11 +79,11 @@ def table_exists(conn) -> bool: ) -def existing_columns(conn) -> set[str]: +def existing_columns(conn) -> dict[str, dict[str, str | None]]: rows = conn.execute( text( """ - SELECT column_name + SELECT column_name, column_type, is_nullable, column_default, extra FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = :table_name @@ -78,7 +91,15 @@ def existing_columns(conn) -> set[str]: ), {"table_name": TABLE_NAME}, ).all() - return {row[0] for row in rows} + return { + row[0]: { + "column_type": row[1], + "is_nullable": row[2], + "column_default": row[3], + "extra": row[4], + } + for row in rows + } def existing_indexes(conn) -> dict[str, tuple[str, ...]]: @@ -154,6 +175,25 @@ def create_index(conn, index_name: str, columns: tuple[str, ...]) -> None: ) +def cleanup_duplicate_fk_indexes(conn) -> None: + indexes = existing_indexes(conn) + duplicates = [ + index_name + for index_name, columns in indexes.items() + if ( + (columns == ("report_id",) and index_name != "idx_report_allocations_report") + or (columns == ("report_item_id",) and index_name != "idx_report_allocations_item") + ) + ] + for index_name in duplicates: + conn.execute( + text( + f"ALTER TABLE {quote_identifier(TABLE_NAME)} " + f"DROP INDEX {quote_identifier(index_name)}" + ) + ) + + def ensure_schema(conn) -> None: conn.execute( text( @@ -195,16 +235,65 @@ def ensure_schema(conn) -> None: for index_name, columns in REQUIRED_INDEXES.items(): create_index(conn, index_name, columns) + cleanup_duplicate_fk_indexes(conn) + + +def normalized_default(value: str | None) -> str | None: + if value is None: + return None + lowered = str(value).strip().lower() + if lowered in {"current_timestamp()", "current_timestamp"}: + return "current_timestamp" + try: + return "zero" if float(lowered) == 0 else lowered + except ValueError: + return lowered + + +def column_matches(column: dict[str, str | None], expected: dict[str, object]) -> list[str]: + mismatches: list[str] = [] + actual_type = str(column["column_type"]).lower() + actual_nullable = str(column["is_nullable"]).upper() + actual_default = normalized_default(column["column_default"]) + actual_extra = str(column["extra"] or "").lower() + + if actual_type != expected["column_type"]: + mismatches.append(f"column_type={column['column_type']}") + if actual_nullable != expected["is_nullable"]: + mismatches.append(f"is_nullable={column['is_nullable']}") + + expected_default = expected.get("default") + if expected_default == "empty_string" and actual_default != "": + mismatches.append(f"column_default={column['column_default']!r}") + elif expected_default == "zero" and actual_default != "zero": + mismatches.append(f"column_default={column['column_default']!r}") + elif expected_default == "current_timestamp" and actual_default != "current_timestamp": + mismatches.append(f"column_default={column['column_default']!r}") + + for extra_text in expected.get("extra_contains", ()): + if extra_text not in actual_extra: + mismatches.append(f"extra={column['extra']!r}") + + return mismatches def verify_schema(conn) -> None: if not table_exists(conn): raise RuntimeError(f"{TABLE_NAME} table is missing") - missing_columns = sorted(REQUIRED_COLUMNS - existing_columns(conn)) + columns = existing_columns(conn) + missing_columns = sorted(set(REQUIRED_COLUMNS) - set(columns)) if missing_columns: raise RuntimeError(f"{TABLE_NAME} missing columns: {', '.join(missing_columns)}") + incompatible_columns = [ + f"{column_name}({', '.join(mismatches)})" + for column_name, expected in REQUIRED_COLUMNS.items() + if (mismatches := column_matches(columns[column_name], expected)) + ] + if incompatible_columns: + raise RuntimeError(f"{TABLE_NAME} incompatible columns: {', '.join(incompatible_columns)}") + indexes = existing_indexes(conn) missing_indexes = [index_name for index_name in REQUIRED_INDEXES if index_name not in indexes] if missing_indexes: