# Конвенция: база данных и идентификаторы Как мы устраиваем таблицы и ключи. Актуальная схема — [../database.md](../database.md). **Взято из проекта jellybit целиком.** Сегодняшний код transcriber этому не следует ни в одном пункте: ключи — UUID v4, а не ULID; время — `time.Now()` по месту вызова в локальной зоне, а не единой точкой в UTC; единой точки генерации и разбора нет. Правила действуют на новый код; переписывание существующего — отдельная работа, и до неё расхождение читается как долг, а не как нарушение. **Механизировано:** одно — сверка изменённого шага схемы с [../database.md](../database.md), шаг гейта `docs.py check` ([README.md](README.md), «Механизировано»). Под прочие пункты ни правила линтера, ни теста-сканера в transcriber нет. ## Первичные ключи — 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`). Единая точка генерации — приложение, а не умолчание в схеме: так забытая вставка падает громко. Измерение длительности — не метка времени. - Миграции — шаги PocketBase на Go (`internal/adapter/repo/pocketbase/migrations`, файл на шаг): коллекции и их поля заводятся кодом. При изменении структуры обновляем схему [../database.md](../database.md) тем же изменением. - Время в **сыром запросе** кладётся и сравнивается тем же видом, каким хранилище пишет свои `created`/`updated`. Сравнение строк побайтово, и разошедшийся вид обращает условие в постоянную истину или ложь — молча. - Выборка «следующей» записи с `LIMIT 1` дополняется ключом в `ORDER BY`: сравнение по неуникальному значению делает порядок обработки невоспроизводимым.