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

122 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Схема базы данных
Актуальная схема SQLite-хранилища: таблицы, поля и связи. Это **живой**
документ — его поддерживаем в соответствии с миграциями.
> **Поддержка вместе с миграциями.** Источник истины по схеме —
> `internal/store/migrations/*.sql` (goose). При **каждой** новой миграции,
> меняющей структуру (таблица/столбец/индекс/связь), обновляем эту диаграмму
> в том же change. Расхождение схемы с миграциями считаем багом
> документации.
>
> Состояние на: миграции `0001_init`, `0002_recognition_plan`,
> `0003_source_miss_count`.
Назначение таблиц и почему так — [architecture.md](architecture.md) →
«Хранилище». Значения `state` и переходы — [workflow.md](workflow.md).
## ER-диаграмма
```mermaid
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`: удаление загрузки уносит её распознавания, подсказки,
правки и ссылки.
- `download``file_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`).