- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
75 lines
6.5 KiB
Markdown
75 lines
6.5 KiB
Markdown
# Конвенция: база данных и идентификаторы
|
||
|
||
Как мы устраиваем таблицы и ключи. Актуальная схема — [../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`:
|
||
сравнение по неуникальному значению делает порядок обработки
|
||
невоспроизводимым.
|