Files
transcriber/docs/conventions/database.md
T
av 2c12376262 docs: ссылки на упразднённый роадмап переадресованы, у восьми фактов назван дом
- ссылки на tasks/ROADMAP.md переведены на BACKLOG.md и на openspec/specs,
  упоминания целей — на задачи, которые эту работу делают;
- судьи документации нашли восемь расхождений: Purpose спеки pipeline объявлял
  неописанным то, что уже нормирован пятью требованиями, вид времени в
  конвенции спорил со схемой, а квоты, шесть часов и отказ от Web Push жили
  сразу в двух документах без ссылки друг на друга;
- два числа получили провенанс: 259 200 запросов в сутки и потолок в шесть
  часов теперь ведут к записке разведки, а не читаются как замер.
2026-08-13 16:00:31 +03:00

6.6 KiB

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

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

Взято из проекта jellybit целиком. Сегодняшний код transcriber следует этому частью: ключи — UUID v4, а не ULID, и единой точки их генерации нет. Время единой точкой читается с 2026-08-13 — internal/clock, метка в UTC, — и правило держит линтер. Правила действуют на новый код; переписывание существующего — отдельная работа, и до неё расхождение читается как долг, а не как нарушение.

Механизировано: сверка изменённого шага схемы с ../database.md (docs.py check), чтение времени единой точкой (forbidigo плюс internal/clock) и согласованность колонок очереди (тест-сканер internal/archrules). Прочие пункты — прозой; адреса — go-linters.md, «Механизировано».

Первичные ключи — ULID, не автоинкремент

  • PK сущности — TEXT ULID (26 символов Crockford base32), генерируется приложением в момент создания записи. Расхождение: идентификаторы записей выдаёт хранилище — 15 знаков собственного алфавита. Своей точки генерации у приложения нет, и ORDER BY id хронологией не является: порядок берут по колонке времени с ключом.
  • Почему ULID: сортируем по времени создания (ORDER BY id = хронология), компактен и удобен в URL и логах (без дефисов — grep и двойной клик берут id целиком), глобально уникален между таблицами — поиск по голому id находит все записи сущности в логах.
  • Точка генерации и разбора одна: создание — при вставке записи в репозитории, разбор — на входных границах. Самодельных генераторов по месту вызова не заводим.

Канонический вид — lowercase

  • Генерим и храним id в нижнем регистре. Сравнение строк в SQLite побайтовое, поэтому любой внешний id (URL, форма, поле запроса) обязательно проходит разбор до запроса к БД — разбор проверяет формат и нормализует регистр (base32 ULID нечувствителен к регистру при декодировании).
  • Синтаксически неверный id считаем несуществующей сущностью (404), без похода в БД.

Естественные и составные ключи — для деталей

  • У таблиц-деталей и связей допустим естественный или составной ключ вместо ULID, когда он есть по природе данных. Отдельный ULID там — мёртвый вес.
  • Прочие генерируемые идентификаторы — тем же способом, что и ключи сущностей: единый формат, сортируемость, корреляция в логах.

Прочее

  • Enum-поля (state, source, …) — обычный TEXT без CHECK; допустимые значения держит код. Расхождение: перечень состояний задачи закрыт схемой (SelectField), а не кодом — ради панели владельца: правка руками не должна заводить состояние, которого конвейер не знает. Цена названа: шестое состояние потребует нового шага схемы.
  • Временные метки — TEXT в RFC 3339, UTC (суффикс Z), например 2006-01-02T15:04:05Z (секундная точность). Фиксированная ширина сохраняет лексикографическую сортировку TEXT, то есть хронологию (ORDER BY created_at). Единая точка генерации — приложение, а не умолчание в схеме: так забытая вставка падает громко. Измерение длительности — не метка времени. Расхождение: вид времени задаёт хранилище — 2006-01-02 15:04:05.000Z, пробел вместо T и доли секунды (../database.md, «Время»). Правило RFC 3339 действует на то, что пишем мы сами мимо хранилища; вид хранилища не меняем — сравнение строк в сыром запросе побайтово, и разошедшийся вид молча обращает условие срока захвата в константу.
  • Миграции — шаги PocketBase на Go (internal/adapter/repo/pocketbase/migrations, файл на шаг): коллекции и их поля заводятся кодом. При изменении структуры обновляем схему ../database.md тем же изменением.
  • Время в сыром запросе кладётся и сравнивается тем же видом, каким хранилище пишет свои created/updated. Сравнение строк побайтово, и разошедшийся вид обращает условие в постоянную истину или ложь — молча.
  • Выборка «следующей» записи с LIMIT 1 дополняется ключом в ORDER BY: сравнение по неуникальному значению делает порядок обработки невоспроизводимым.