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