"""Phase 4:Job(异步任务)与 Experiment(实验归档)表。 `result_json` 使用 MEDIUMTEXT(MySQL 上限 16MB):全市场多年回测的结果含 净值/回撤曲线、逐笔成交、Signal↔Fill 记录与个股收益曲线,实测可达数 MB, MySQL `TEXT`(64KB)会直接报 1406 Data too long 导致 Job 归档失败 (2026-09 高股息案例实测:约 1.2MB → 落库失败)。 SQLite 不区分 TEXT 长度,故模型层统一用 MEDIUMTEXT.with_variant 保持跨库可用。 """ from __future__ import annotations from datetime import datetime from sqlalchemy import DateTime, String, Text from sqlalchemy.dialects.mysql import MEDIUMTEXT from sqlalchemy.orm import Mapped, mapped_column from app.infrastructure.persistence.sqlalchemy.base import Base # 长 JSON 列类型:MySQL 用 MEDIUMTEXT(16MB),其它方言退化为 TEXT(SQLite 无长度限制) _LONG_JSON = Text().with_variant(MEDIUMTEXT(), "mysql") class JobModel(Base): __tablename__ = "job" id: Mapped[str] = mapped_column(String(32), primary_key=True) kind: Mapped[str] = mapped_column(String(16)) status: Mapped[str] = mapped_column(String(12), index=True) stage: Mapped[str | None] = mapped_column(String(24), nullable=True) spec_json: Mapped[str] = mapped_column(Text) error: Mapped[str | None] = mapped_column(Text, nullable=True) result_json: Mapped[str | None] = mapped_column(_LONG_JSON, nullable=True) experiment_id: Mapped[str | None] = mapped_column(String(32), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime) started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) class ExperimentModel(Base): __tablename__ = "experiment" id: Mapped[str] = mapped_column(String(32), primary_key=True) kind: Mapped[str] = mapped_column(String(16)) spec_json: Mapped[str] = mapped_column(Text) result_json: Mapped[str] = mapped_column(_LONG_JSON) summary_text: Mapped[str | None] = mapped_column(String(200), nullable=True) code_version: Mapped[str | None] = mapped_column(String(40), nullable=True) data_version: Mapped[str | None] = mapped_column(String(40), nullable=True) job_id: Mapped[str | None] = mapped_column(String(32), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime)