Все сущности переехали с INTEGER AUTOINCREMENT на TEXT ULID (lowercase, internal/ident — единая точка генерации и разбора; oklog/ulid). Инфохэши загрузки — множество (download_infohash, v1/v2 гибридных торрентов): дедуп и сопоставление в поллинге по любому из хешей, magnet-парсер отдаёт оба хеша гибридной ссылки, усечённый v2-хеш v2-only раздач не хранится. Инвариант «не более одной активной загрузки на infohash» вместо снятого unique-индекса держат guarded-методы store в одной write-транзакции (_txlock=immediate): CreateDownloadIfNoActive (приём/adopt, с доносом недостающих хешей), ActivateIfNoOtherActive (retry/recovery/relink, отказ до побочных эффектов), guarded AddInfohashes; SetDownloadState отклоняет терминал→активное как механический бэкстоп. Миграция 0006 — первая Go-миграция goose: пересоздание таблиц при включённых FK, backfill ULID с timestamp из created_at (хронология id сохранена), разнос infohash, удаление idempotency_key. BREAKING: формат id в URL/логах/Telegram, REST-поля id (string) и infohashes (список). Новая конвенция docs/conventions/database.md (без числовых PK), корреляция в логах grep'ом по голому ULID, ER-схема обновлена. Спеки: новая capability identity, MODIFIED в state-reconciliation; change заархивирован. Пройдены ревью дизайна и кода (по 8 углов), все находки исправлены с регрессионными тестами. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.3 KiB
5.3 KiB
1. Фундамент: пакет ident
- 1.1 Добавить зависимость
github.com/oklog/ulid/v2; пакетinternal/ident:NewID()(lowercase, monotonic entropy, потокобезопасно),NewIDAt(t time.Time)(для миграции/бэкфилла),Parse(s)(нормализация регистра + валидация); тесты (lowercase, сортируемость, отказ на мусоре)
2. Схема и миграция
- 2.1 Механизм Go-миграций goose в
internal/store/migrations(регистрация черезgoose.AddMigrationContext, совместный прогон с embed SQL-миграциями) - 2.2 Миграция 0006: новые таблицы с TEXT ULID PK (все шесть), перенос
данных с маппингом
int → ULID(timestamp изcreated_at, fallback — время миграции), переносdownload.infohash→download_infohash(lowercase,kindпо длине hex), удалениеdownload.infohashиdownload.idempotency_key, пересоздание индексов - 2.3 Тест миграции на фикстурной БД: FK-связи сохранены, порядок по
id= порядок поcreated_at, хеши разнесены,idempotency_keyотсутствует
3. Store
- 3.1 Типы id
int64 → stringво всех структурах и методахstore(download, recognition, hint, override, metadata_candidate, file_link, list); генерация ULID черезident.NewID()во ВСЕХ Create-методах (вместоLastInsertId) - 3.2 Guarded-методы инварианта (design D4):
CreateDownloadIfNoActiveиActivateIfNoOtherActive, каждый — одна write-транзакция;FindActiveByInfohash/ExistsByInfohashjoin'ом по любому хешу (для чтения); убрать CASE-восстановлениеidempotency_keyизSetDownloadState - 3.3 Методы хешей: добавить недостающие хеши загрузке (INSERT OR IGNORE), получать хеши вместе с Download (срез в модели)
- 3.4 Поиск в списке (
listWhere):EXISTS-подзапрос поdownload_infohashвместоLIKEпо удаляемомуdownload.infohash
4. Ядро и воркер
- 4.1 Приём (
ingest) и discover-adopt — черезCreateDownloadIfNoActive; хеш из magnet (btih ИЛИ btmh,kindпо длине hex) пишется в той же транзакции; сигнатурыResult.DownloadID,notifyFailed,Notifier.Notify,failNotified— на string - 4.2 Поллинг/сверка: сопоставление раздачи по любому из хешей загрузки (Poll, desync, recovery, preflight); дописывание недостающих v1/v2, когда qBittorrent отдаёт оба; scoped-логгеры — первый известный хеш
- 4.3 Retry, recovery-воскрешение и Relink — через
ActivateIfNoOtherActive(сейчас гонку закрывал unique-индекс — см. design D4); понятная ошибка при занятом хеше - 4.4
apply_batch_idгенерировать черезident.NewID()
5. Внешние границы
- 5.1
httpapi:ident.ParseвpathID(невалидный → 404 без похода в БД) и дляcandidate_idиз формы ревью; сентинелыdownloadID > 0 → != ""; REST DTO:idstring,infohash→ списокinfohashes; карточка показывает все хеши с копированием; live-лукап по любому хешу; проверить шаблоны и ссылки - 5.2 Telegram (
tgbot):parseCallbackи callback-data на string-id,pending map[int64]int64 → map[int64]string, сентинелid == 0 → == "", ссылки/review/{id}; понятный ответ на устаревшую callback-data со старым числовым id
6. Логи и документация
- 6.1 Атрибуты
<entity>_idв логах:recognition_idу попыток распознавания,batch_idу раскладки; сверить с scoped-логгером - 6.2
docs/conventions/logging.md: примеры id в формате ULID, grep по голому id как штатная корреляция - 6.3 Новая
docs/conventions/database.md(TEXT ULID PK, без AUTOINCREMENT, естественные ключи у деталей, lowercase + нормализация); ссылки изdocs/conventions/README.mdиCLAUDE.md - 6.4 ER-схема
docs/specs/database.md: ULID PK,download_infohash, удалённые столбцы
7. Проверка
- 7.1
task testиtask lintзелёные; ручной прогон: приём magnet → дедуп повторного приёма → страница/download/{id}с ULID в URL