docs/ по канону 12: паспорт с целью проекта, архитектура сегодняшнего устройства, схема хранилища, модель угроз, конвенции кода, журнал ревью. Конвенции перенесены из jellybit; места, где код им не следует, помечены строкой «Расхождение» как объявленный долг. tasks/ с роадмапом: две достигнутые цели, две запланированные (веб и многопользовательский режим), два направления (все форматы, долгие записи) и пять задач в беклоге. openspec/config.yaml — маршрутизатор с адресами документов, спек пока нет. CLAUDE.md переписан по форме канона: инварианты с severity, семантика гейта, запреты с путями. Taskfile получил task gate.
57 lines
4.6 KiB
Markdown
57 lines
4.6 KiB
Markdown
# Конвенция: база данных и идентификаторы
|
|
|
|
Как мы устраиваем таблицы и ключи. Актуальная схема — [../database.md](../database.md).
|
|
|
|
**Взято из проекта jellybit целиком.** Сегодняшний код transcriber этому не
|
|
следует ни в одном пункте: ключи — UUID v4, а не ULID; время — `time.Now()` по
|
|
месту вызова в локальной зоне, а не единой точкой в UTC; единой точки генерации
|
|
и разбора нет. Правила действуют на новый код; переписывание существующего —
|
|
отдельная работа, и до неё расхождение читается как долг, а не как нарушение.
|
|
|
|
**Механизировано:** ничего. Ни правила линтера, ни теста-сканера под эти пункты
|
|
в transcriber нет.
|
|
|
|
## Первичные ключи — ULID, не автоинкремент
|
|
|
|
- **PK сущности — TEXT ULID** (26 символов Crockford base32), генерируется
|
|
**приложением** в момент создания записи.
|
|
- Почему ULID: сортируем по времени создания (`ORDER BY id` = хронология),
|
|
компактен и удобен в URL и логах (без дефисов — grep и двойной клик берут id
|
|
целиком), глобально уникален между таблицами — поиск по голому id находит все
|
|
записи сущности в логах.
|
|
- **Точка генерации и разбора одна**: создание — при вставке записи в
|
|
репозитории, разбор — на входных границах. Самодельных генераторов по месту
|
|
вызова не заводим.
|
|
|
|
## Канонический вид — lowercase
|
|
|
|
- Генерим и храним id в **нижнем регистре**. Сравнение строк в SQLite
|
|
побайтовое, поэтому любой внешний id (URL, форма, поле запроса) обязательно
|
|
проходит разбор до запроса к БД — разбор проверяет формат и нормализует
|
|
регистр (base32 ULID нечувствителен к регистру при декодировании).
|
|
- Синтаксически неверный id считаем несуществующей сущностью (404), без похода
|
|
в БД.
|
|
|
|
## Естественные и составные ключи — для деталей
|
|
|
|
- У таблиц-деталей и связей допустим естественный или составной ключ вместо
|
|
ULID, когда он есть по природе данных. Отдельный ULID там — мёртвый вес.
|
|
- Прочие генерируемые идентификаторы — тем же способом, что и ключи сущностей:
|
|
единый формат, сортируемость, корреляция в логах.
|
|
|
|
## Прочее
|
|
|
|
- Enum-поля (`state`, `source`, …) — обычный `TEXT` без `CHECK`; допустимые
|
|
значения держит код.
|
|
- Временные метки — `TEXT` в **RFC 3339, UTC (суффикс `Z`)**, например
|
|
`2006-01-02T15:04:05Z` (секундная точность). Фиксированная ширина сохраняет
|
|
лексикографическую сортировку TEXT, то есть хронологию (`ORDER BY created_at`).
|
|
Единая точка генерации — приложение, а не умолчание в схеме: так забытая
|
|
вставка падает громко. Измерение длительности — не метка времени.
|
|
- Миграции — goose (`migrations/`): SQL-файлы для DDL; Go-миграции
|
|
(`goose.AddMigrationContext`) — когда нужен код (генерация id, заполнение
|
|
задним числом). При изменении структуры обновляем схему
|
|
[../database.md](../database.md) тем же изменением.
|
|
- Добавляя колонку, соблюдай инвариант «Новая колонка правится во всех четырёх
|
|
местах» — [CLAUDE.md](../../CLAUDE.md), «Инварианты».
|