# Схема базы данных Актуальная схема 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" TEXT url "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`).