"""Журнал прогонов парсинга (parse_runs) + ссылка на последний прогон источника. Revision ID: 005 Revises: 004 Create Date: 2026-08-27 Прогресс заливки раньше был не виден: у источника был только last_status (idle/running/done/error) без «сколько из скольки». Таблица parse_runs хранит счётчики и структурный журнал каждого прогона — по ним админка рисует шкалу загрузки и показывает, на чём именно споткнулся источник. """ import sqlalchemy as sa from alembic import op revision = "005" down_revision = "004" branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "parse_runs", sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column( "source_id", sa.Integer(), sa.ForeignKey("parse_sources.id", ondelete="CASCADE"), nullable=False, ), sa.Column("celery_task_id", sa.String(64), nullable=True), # queued / running / done / partial / error / cancelled sa.Column("status", sa.String(20), nullable=False, server_default="queued"), # queued / fetch / index / finished sa.Column("stage", sa.String(20), nullable=False, server_default="queued"), sa.Column("target", sa.Integer(), nullable=False, server_default="0"), sa.Column("fetched", sa.Integer(), nullable=False, server_default="0"), sa.Column("processed", sa.Integer(), nullable=False, server_default="0"), sa.Column("added", sa.Integer(), nullable=False, server_default="0"), sa.Column("duplicates", sa.Integer(), nullable=False, server_default="0"), # Мусор от источника (нет title/ext_id) — считаем отдельно от ошибок sa.Column("skipped", sa.Integer(), nullable=False, server_default="0"), sa.Column("failed", sa.Integer(), nullable=False, server_default="0"), # Флаг кооперативной отмены: воркер проверяет его на каждом тике прогресса sa.Column("cancel_requested", sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column("error", sa.Text(), nullable=True), sa.Column("log", sa.JSON(), nullable=True), sa.Column("started_at", sa.DateTime(), server_default=sa.func.now(), nullable=False), sa.Column("heartbeat_at", sa.DateTime(), nullable=True), sa.Column("finished_at", sa.DateTime(), nullable=True), ) op.create_index("ix_parse_runs_source_id", "parse_runs", ["source_id"]) op.create_index("ix_parse_runs_status", "parse_runs", ["status"]) op.create_index("ix_parse_runs_started_at", "parse_runs", ["started_at"]) # Последний (он же текущий, пока идёт) прогон источника — чтобы список # источников отдавался одним джойном, без подзапроса «максимальный id». op.add_column("parse_sources", sa.Column("last_run_id", sa.Integer(), nullable=True)) def downgrade() -> None: op.drop_column("parse_sources", "last_run_id") op.drop_index("ix_parse_runs_started_at", table_name="parse_runs") op.drop_index("ix_parse_runs_status", table_name="parse_runs") op.drop_index("ix_parse_runs_source_id", table_name="parse_runs") op.drop_table("parse_runs")