From f9d7fd92163ff579b8a8c14a4aec275f040f6da0 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Mon, 29 Jun 2026 10:29:29 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=81=D1=85=D0=B5=D0=BC=D1=83=20=D0=B1=D0=B0=D0=B7=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 3 + docs/specs/architecture.md | 4 +- docs/specs/database.md | 119 +++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 docs/specs/database.md diff --git a/CLAUDE.md b/CLAUDE.md index d6b25d0..b6a2a39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -124,6 +124,9 @@ Module path — `git.vakhrushev.me/av/jellybit`. Go 1.26, `CGO_ENABLED=0`. файл (`config.toml` не коммитится, `0600`), не в env; валидация на старте: [docs/conventions/config.md](docs/conventions/config.md). - Время — всегда с явным TZ (сервер в `Europe/Moscow`). +- Миграции БД (goose, `internal/store/migrations`) — при изменении структуры + (таблица/столбец/индекс/связь) в том же change обновляем ER-схему + [docs/specs/database.md](docs/specs/database.md). Кросс-каттинг конвенции (как пишем код, а не что система делает) живут в [docs/conventions/](docs/conventions/README.md) и не переносятся в OpenSpec. diff --git a/docs/specs/architecture.md b/docs/specs/architecture.md index 1122649..ca6796d 100644 --- a/docs/specs/architecture.md +++ b/docs/specs/architecture.md @@ -73,7 +73,9 @@ reject / defer / undo) — команды к `worker`: ## Хранилище -SQLite. Схема покрывает приём, цикл ревью и откат: +SQLite. Полная схема (таблицы, поля, связи) — [database.md](database.md), +поддерживается вместе с миграциями. Схема покрывает приём, цикл ревью и +откат: - `download` — `id`, тип и значение источника, контекст, `infohash`, `idempotency_key`, состояние, `error_code`/`error_msg`, тайминги. diff --git a/docs/specs/database.md b/docs/specs/database.md new file mode 100644 index 0000000..f0af67f --- /dev/null +++ b/docs/specs/database.md @@ -0,0 +1,119 @@ +# Схема базы данных + +Актуальная схема SQLite-хранилища: таблицы, поля и связи. Это **живой** +документ — его поддерживаем в соответствии с миграциями. + +> **Поддержка вместе с миграциями.** Источник истины по схеме — +> `internal/store/migrations/*.sql` (goose). При **каждой** новой миграции, +> меняющей структуру (таблица/столбец/индекс/связь), обновляем эту диаграмму +> в том же change. Расхождение схемы с миграциями считаем багом +> документации. +> +> Состояние на: миграции `0001_init`, `0002_recognition_plan`. + +Назначение таблиц и почему так — [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" + 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`).