# Схема хранилища СУБД, миграции, правило времени и идентификаторов. СУБД — SQLite, драйвер `mattn/go-sqlite3` (нужен CGO). Запросы строит `doug-martin/goqu` с диалектом `sqlite3`. Миграции — `pressly/goose`, каталог `migrations/`, вшит в бинарник через `//go:embed migrations/*.sql` в `main.go` и накатывается при старте. Новый файл достаточно положить в каталог. **Идентификаторы** — UUID v4 строкой. **Время** — локальная зона процесса, UTC не навязан. Колонки `created_at` и `updated_at` проставляет приложение, а не СУБД; умолчание `CURRENT_TIMESTAMP` стоит только у `files.created_at`. Того, что единой точки генерации идентификатора и времени нет, здесь не повторяем: перечень единых точек и их отсутствий держит [architecture.md](architecture.md), «Единые точки проекта». Переход на PocketBase запланирован, но не начат — здесь описано сегодняшнее состояние. Открытые вопросы перехода — в [architecture.md](architecture.md), раздел «Открытые вопросы». ## Таблицы ### `files` Один файл на одну физическую копию: исходник, результат конвертации и копия в Object Storage — три разные записи. | Колонка | Тип | Что | | --- | --- | --- | | `id` | TEXT PK | UUID файла | | `storage` | TEXT | `local` или `s3` | | `file_name` | TEXT | Имя в хранилище: UUID с расширением | | `size` | INTEGER | Размер в байтах | | `created_at` | DATETIME | Умолчание `CURRENT_TIMESTAMP` | ### `transcribe_jobs` Задача расшифровки и она же очередь. | Колонка | Тип | Что | | --- | --- | --- | | `id` | TEXT PK | UUID задачи | | `state` | TEXT | `created`, `converted`, `transcribe`, `done`, `failed` | | `source` | TEXT | `api`, `telegram`, `unknown`; умолчание `unknown` | | `file_id` | TEXT FK → `files.id` | **Текущий** файл задачи: шаг конвейера переставляет ссылку на свой результат | | `delay_time` | DATETIME | Не брать задачу раньше этого времени | | `acquisition_id` | TEXT | Кто захватил задачу | | `acquire_time` | DATETIME | Когда захватил; по нему считается протухание | | `recognition_op_id` | TEXT | Идентификатор операции в Yandex Cloud | | `transcription_text` | TEXT | Результат распознавания | | `is_error` | BOOLEAN | Задача с `1` из выборки исключена навсегда | | `error_text` | TEXT | Текст ошибки, машинный | | `tg_chat_id` | INTEGER | Куда отправить результат | | `tg_reply_message_id` | INTEGER | С каким сообщением связать | | `created_at`, `updated_at` | DATETIME | Проставляет приложение | Индексов, кроме первичных ключей, нет. Выборка воркера идёт полным перебором по `state`, `is_error`, `delay_time` и `acquire_time`. ## Представление данных Чем физически лежит запись и что происходит при чтении и записи. - **Расшифровка лежит целиком в колонке `transcription_text`** одной строкой. Запись длиной в час даёт десятки килобайт в одной ячейке; читается она целиком при каждом `GetByID` и при каждом захвате задачи воркером. - **Аудио в базе не лежит.** На диске — каталог `data/files`, плоский, имя файла равно UUID с расширением. Ни файлы, ни объекты в Object Storage не удаляются после завершения задачи: каталог и бакет растут неограниченно. - **Захват задачи — два запроса подряд, не транзакция.** Сперва `UPDATE … WHERE id = (SELECT … LIMIT 1)` проставляет `acquisition_id`, затем отдельный `SELECT … WHERE acquisition_id = ?` читает строку. Репозиторий сверяет число затронутых строк с ожидаемым, но между запросами задачу может перехватить другой воркер с тем же значением — на одном процессе это не наблюдалось. - **Список колонок задан не одним местом** — четырьмя запросами файла `internal/adapter/repo/sqlite/transcript_job_repo.go`. Правило правки всех четырёх и его severity — инвариант в [CLAUDE.md](../CLAUDE.md), «Инварианты». ## Настройки с числовым значением | Настройка | Значение | Где | | --- | --- | --- | | Срок захвата, конвертация и распознавание | 1 час | `service/transcribe.go`, вызовы `findJob` | | Срок захвата, проверка операции | 24 часа | там же | | Пауза воркера между попытками | 1 секунда | `controller/worker/worker.go` | | Задержка перед первой проверкой операции | 10 секунд | `service/transcribe.go` | | Задержка между проверками операции | 5 секунд | там же | | Предел длины сообщения Telegram | 4000 символов | `adapter/telegram/sender.go` | | Память под multipart-загрузку | 32 МиБ | `main.go`, `router.MaxMultipartMemory` | | Таймаут мягкой остановки | 5 секунд | конфиг, `[server] shutdown_timeout` | | Таймаут жёсткой остановки | 20 секунд | конфиг, `[server] force_shutdown_timeout` | | Таймаут обновлений Telegram | 10 секунд | конфиг, `[telegram] update_timeout` | | Качество кодирования vorbis | `-q:a 4` | `adapter/converter/ffmpeg/ffmpeg.go` | Чего среди настроек **нет**: режим журналирования SQLite не задан (значение по умолчанию, не WAL), таймаут занятости не задан, размер пула соединений не задан, срока хранения файлов и объектов нет вовсе. Таймаутов у обращений к Telegram, S3 и SpeechKit тоже нет — ни одного.