Files
transcriber/docs/conventions/database.md
T
av c9b7765646 хранилище переехало с PocketBase на SQLite со своим каталогом файлов
- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose
  под файловым замком, одна миграция начальной схемы вместо семи прежних
- транспорт переписан на net/http: свои слои, свой ограничитель частоты,
  отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли
- по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета
  читается справа налево, узнавание известного идёт читающим пулом
2026-08-23 08:06:04 +03:00

75 lines
6.5 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.
# Конвенция: база данных и идентификаторы
Как мы устраиваем таблицы и ключи. Актуальная схема — [../database.md](../database.md).
**Взято из проекта jellybit целиком.** Сегодняшний код transcriber следует этому
целиком: ключи — ULID в нижнем регистре, выдаёт их единая точка `internal/ident`
(с 2026-08-22, задача `storage-without-pocketbase`), время читает единая точка
`internal/clock` (с 2026-08-13), и правило времени держит линтер. Расхождений у
записи не осталось.
**Механизировано:** сверка изменённого шага схемы с
[../database.md](../database.md) (`docs.py check`), чтение времени единой точкой
(`forbidigo` плюс `internal/clock`) и согласованность колонок очереди
(тест-сканер `internal/archrules`). Прочие пункты — прозой; адреса —
[go-linters.md](go-linters.md), «Механизировано».
## Первичные ключи — ULID, не автоинкремент
- **PK сущности — TEXT ULID** (26 символов Crockford base32), генерируется
**приложением** в момент создания записи. Выдача монотонна внутри одной
миллисекунды: колонка времени несёт секунды, и порядок записей одной секунды
задаёт ключ. Порядок ленты берут парой «время заведения и ключ» — одного
времени мало.
- Почему ULID: сортируем по времени создания (`ORDER BY id` = хронология),
компактен и удобен в URL и логах (без дефисов — grep и двойной клик берут id
целиком), глобально уникален между таблицами — поиск по голому id находит все
записи сущности в логах.
- **Точка генерации и разбора одна** — `internal/ident`: `New` выдаёт, `Parse`
разбирает пришедшее снаружи. Самодельных генераторов по месту вызова не
заводим.
## Канонический вид — lowercase
- Генерим и храним id в **нижнем регистре**. Сравнение строк в SQLite
побайтовое, поэтому любой внешний id (URL, форма, поле запроса) обязательно
проходит разбор до запроса к БД — разбор проверяет формат и нормализует
регистр (base32 ULID нечувствителен к регистру при декодировании).
- Синтаксически неверный id считаем несуществующей сущностью (404), без похода
в БД.
## Естественные и составные ключи — для деталей
- У таблиц-деталей и связей допустим естественный или составной ключ вместо
ULID, когда он есть по природе данных. Отдельный ULID там — мёртвый вес.
- Прочие генерируемые идентификаторы — тем же способом, что и ключи сущностей:
единый формат, сортируемость, корреляция в логах.
## Прочее
- Enum-поля (`state`, `halt_reason`, …) — обычный `TEXT` без `CHECK`; допустимые
значения держит код. Прежде часть перечней закрывала схема — правку руками вела
панель владельца, и она вправе была завести значение, которого сервис не
знает. Панели нет с 2026-08-22, правка идёт только нашим кодом, и закрытый
перечень в схеме остался бы ценой — новое значение стоило бы нового шага — без
покупателя.
- Временные метки — `TEXT` в **RFC 3339, UTC (суффикс `Z`)**, например
`2006-01-02T15:04:05Z` (секундная точность). Фиксированная ширина сохраняет
лексикографическую сортировку TEXT, то есть хронологию (`ORDER BY created_at`).
Единая точка генерации — приложение, а не умолчание в схеме: так забытая
вставка падает громко. Измерение длительности — не метка времени.
Умолчаний вида `CURRENT_TIMESTAMP` в схеме нет ни у одной колонки, и вид один
на все — включая те, что пишет только сам сервис: своего типа времени у SQLite
нет, а колонка, заполненная то одним видом, то другим, молча обращает условие
срока захвата в константу.
- Миграции — шаги `pressly/goose/v3` на Go
(`internal/adapter/repo/sqlite/migrations`, файл на шаг, версия — число в
начале имени): таблицы, их колонки и индексы заводятся кодом. При изменении
структуры обновляем схему [../database.md](../database.md) тем же изменением.
- Время в запросе кладётся и сравнивается тем же видом, каким оно лежит в
колонке. Сравнение строк побайтово, и разошедшийся вид обращает условие в
постоянную истину или ложь — молча.
- Выборка «следующей» записи с `LIMIT 1` дополняется ключом в `ORDER BY`:
сравнение по неуникальному значению делает порядок обработки
невоспроизводимым.