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

6.5 KiB
Raw Blame History

Конвенция: база данных и идентификаторы

Как мы устраиваем таблицы и ключи. Актуальная схема — ../database.md.

Взято из проекта jellybit целиком. Сегодняшний код transcriber следует этому целиком: ключи — ULID в нижнем регистре, выдаёт их единая точка internal/ident (с 2026-08-22, задача storage-without-pocketbase), время читает единая точка internal/clock (с 2026-08-13), и правило времени держит линтер. Расхождений у записи не осталось.

Механизировано: сверка изменённого шага схемы с ../database.md (docs.py check), чтение времени единой точкой (forbidigo плюс internal/clock) и согласованность колонок очереди (тест-сканер internal/archrules). Прочие пункты — прозой; адреса — 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 тем же изменением.
  • Время в запросе кладётся и сравнивается тем же видом, каким оно лежит в колонке. Сравнение строк побайтово, и разошедшийся вид обращает условие в постоянную истину или ложь — молча.
  • Выборка «следующей» записи с LIMIT 1 дополняется ключом в ORDER BY: сравнение по неуникальному значению делает порядок обработки невоспроизводимым.