Files
jellybit/docs/specs/database.md
T
avandClaude Opus 4.8 cc7e51b3a4 Обработка рассинхрона состояния с реальностью (state-reconciliation)
Распознаём ручное удаление источника (раздача в qBittorrent) и/или цели
(разложенные хардлинки) и отражаем его в состоянии задачи, без автодействий.

- Новая capability state-reconciliation (OpenSpec): фоновая сверка по матрице
  «источник × цель» → состояния target_missing/orphaned/deleted, переходы и
  самовосстановление (healing).
- worker: reconcileDesync в Poll (только разложенные/desync-задачи), дебаунс
  пропажи источника (порог [worker].source_missing_threshold) и синхронный
  preflight перед действиями (relink/recognize/apply/undo) — не доверяем
  state в БД.
- layout.Undo: отказ снять последнюю копию (nlink<=1 или нет источника),
  отказ всего батча без частичного отката (ErrLastCopy).
- store: единый список terminalStates для IsTerminal и FindActiveByInfohash
  (иначе семантика «активности» разъезжается), столбец source_miss_count,
  миграция 0003.
- httpapi/web и Telegram: показ новых состояний и уведомления о рассинхроне.
- Доки: workflow.md, jellyfin-layout.md, database.md (+0003), config.

Change заархивирован в openspec/changes/archive, дельта влита в openspec/specs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:07:09 +03:00

5.8 KiB
Raw Blame History

Схема базы данных

Актуальная схема SQLite-хранилища: таблицы, поля и связи. Это живой документ — его поддерживаем в соответствии с миграциями.

Поддержка вместе с миграциями. Источник истины по схеме — internal/store/migrations/*.sql (goose). При каждой новой миграции, меняющей структуру (таблица/столбец/индекс/связь), обновляем эту диаграмму в том же change. Расхождение схемы с миграциями считаем багом документации.

Состояние на: миграции 0001_init, 0002_recognition_plan, 0003_source_miss_count.

Назначение таблиц и почему так — architecture.md → «Хранилище». Значения state и переходы — workflow.md.

ER-диаграмма

erDiagram
    download ||--o{ recognition : "распознавания"
    download ||--o{ hint : "подсказки"
    download ||--o{ override : "ручные правки"
    download ||--o{ file_link : "хардлинки"
    recognition ||--o{ metadata_candidate : "кандидаты базы"

    download {
        INTEGER id PK "AUTOINCREMENT"
        TEXT    source_type "NOT NULL; magnet|torrent|url"
        TEXT    source_ref  "NOT NULL; magnet/url/путь"
        TEXT    context     "NOT NULL DEFAULT ''"
        TEXT    infohash    "nullable; может появиться позже приёма"
        TEXT    idempotency_key "nullable; UNIQUE если NOT NULL"
        TEXT    state       "NOT NULL; см. workflow.md"
        TEXT    error_code  "nullable"
        TEXT    error_msg   "nullable"
        INTEGER source_miss_count "NOT NULL DEFAULT 0; дебаунс пропажи источника (миграция 0003)"
        TEXT    created_at  "NOT NULL DEFAULT datetime('now')"
        TEXT    updated_at  "NOT NULL DEFAULT datetime('now')"
    }

    recognition {
        INTEGER id PK "AUTOINCREMENT"
        INTEGER download_id FK "NOT NULL; ON DELETE CASCADE"
        INTEGER attempt_no    "NOT NULL DEFAULT 1"
        INTEGER is_current     "NOT NULL DEFAULT 1; 0/1"
        TEXT    media_type     "nullable; movie|series"
        TEXT    title          "nullable"
        TEXT    original_title "nullable"
        INTEGER year           "nullable"
        TEXT    provider       "nullable; tmdb|tvdb|tvmaze|none"
        TEXT    provider_id    "nullable"
        REAL    confidence     "nullable"
        TEXT    reasons        "NOT NULL DEFAULT '[]'; JSON: причины не-авто"
        TEXT    raw_llm        "nullable; сырой ответ LLM"
        TEXT    plan           "nullable; JSON recognize.Plan (миграция 0002)"
        TEXT    created_at     "NOT NULL DEFAULT datetime('now')"
    }

    hint {
        INTEGER id PK "AUTOINCREMENT"
        INTEGER download_id FK "NOT NULL; ON DELETE CASCADE"
        TEXT    text       "NOT NULL"
        TEXT    created_at "NOT NULL DEFAULT datetime('now')"
    }

    override {
        INTEGER id PK "AUTOINCREMENT"
        INTEGER download_id FK "NOT NULL; ON DELETE CASCADE"
        TEXT    field      "NOT NULL; UNIQUE(download_id, field)"
        TEXT    value      "NOT NULL"
        TEXT    created_at "NOT NULL DEFAULT datetime('now')"
    }

    metadata_candidate {
        INTEGER id PK "AUTOINCREMENT"
        INTEGER recognition_id FK "NOT NULL; ON DELETE CASCADE"
        TEXT    provider    "NOT NULL"
        TEXT    provider_id "NOT NULL"
        TEXT    title       "nullable"
        INTEGER year        "nullable"
        INTEGER chosen      "NOT NULL DEFAULT 0; 0/1"
        TEXT    created_at  "NOT NULL DEFAULT datetime('now')"
    }

    file_link {
        INTEGER id PK "AUTOINCREMENT"
        INTEGER download_id FK "NOT NULL; ON DELETE CASCADE"
        TEXT    apply_batch_id "NOT NULL; батч для точечного undo"
        TEXT    src_path "NOT NULL; исходный файл раздачи"
        TEXT    dst_path "NOT NULL; целевой хардлинк"
        TEXT    kind     "NOT NULL; video|subtitle|..."
        TEXT    status   "NOT NULL; linked|..."
        TEXT    created_at "NOT NULL DEFAULT datetime('now')"
    }

Связи и кардинальность

  • download 1 — N recognition / hint / override / file_link; recognition 1 — N metadata_candidate. Все дочерние — с ON DELETE CASCADE: удаление загрузки уносит её распознавания, подсказки, правки и ссылки.
  • downloadfile_link — один источник (раздача) ко многим разложенным файлам; внутри строки file_link связь src_path → dst_path — 1:1. Не каждый файл раздачи попадает в file_link (только распознанные медиа и субтитры); ссылки могут накапливаться несколькими apply_batch_id.

Индексы и ограничения

  • download: UNIQUE(idempotency_key) WHERE idempotency_key IS NOT NULL; индекс по state.
  • recognition: индекс по download_id.
  • override: UNIQUE(download_id, field).
  • metadata_candidate: индекс по recognition_id.
  • file_link: индексы по download_id и по apply_batch_id.

Enum-поля (source_type, state, provider, kind, status, флаги 0/1) на уровне SQLite — обычный TEXT/INTEGER без CHECK; допустимые значения держит код (internal/store).