- фазы сна, контекст пульса и имена тренировок попадают в реестр `category_value` (миграция 00010): строка хранится дословно, выведенный код лежит рядом отдельной записью, а не полем внутри точки - словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в сыром архиве нет - наблюдение входит в отпечаток витрины, выведенный код — нет: он производная от словаря, а не от журнала
7.3 KiB
7.3 KiB
База данных и идентификаторы
Схема как таковая — в database.md; здесь только правила, по которым она пишется.
- Первичные ключи сущностей — TEXT ULID, генерируется приложением
(
internal/ident). Сортируется по времени создания, удобен в логах и URL. Разбор внешнего id —ident.Parseна входной границе; синтаксически невалидный id — 404 без похода в БД. - Естественный ключ вместо ULID там, где он есть по природе данных:
workout— поidиз HealthKit,record— по парерод секции + id(форму идентификатора у пяти из шести секций живьём никто не видел, и несквознойidв двух секциях затёр бы одну запись другой молча). - Новая единица хранения тем же изменением входит в отпечаток витрины и в счётчики отчёта пересборки. Отпечаток отвечает «да/нет» за витрину целиком, и единица, которой нет в счётчиках, делает расхождение безадресным: человек видит «не совпало» при неизменившемся числе объектов и принимает по этому необратимое решение о подмене базы.
- Провенанс, входящий в отпечаток, обязан быть явной функцией журнала.
«Кто первым записал строку» — функция порядка свёртки, а он порядку журнала не
равен: живой приём и пересборка разойдутся при одинаковом журнале. Там, где
провенанс в отпечаток не идёт, слабое правило допустимо и должно быть названо
слабым на месте — иначе его скопируют туда, где оно неверно (
bucketпротивcategory_value). - Колонка, производная от бинаря, а не от журнала, в отпечаток не входит. Кэш чистой функции (код по словарю, справочное имя) в отпечатке превращает всякую правку бинаря в расхождение при побайтно совпавшем журнале — и человек, принимающий по отпечатку необратимое решение о подмене базы, читает это как дефект. Правильность самой производной проверяют её тесты: это другой вопрос, и смешение обесценивает оракул сходимости.
- Граница на число элементов, набираемых из чужого тела, применяется при накоплении, а не при выдаче. Накопитель без границы растёт вместе с телом, а тело контролирует отправитель; отказ по памяти в фоновой горутине не перехватывается, и перезапуск берёт ту же доставку. Усечение при этом обязано остаться функцией множества (например, N наименьших ключей), иначе порядок элементов на проводе решает состав витрины.
- Правило выбора между двумя версиями одних данных объявляется либо функцией множества версий, либо явно функцией порядка журнала — третьего состояния нет. «Побеждает последняя пришедшая» третьим состоянием и является: порядок свёртки порядку журнала не равен, и живая витрина расходится с пересборкой молча.
- Любое значение из чужого JSON, попадающее в ключ, в лог или в отчёт, имеет
названный предел длины (имена непокрытых секций,
idсущности). - Колонка, по которой принимается необратимое решение, отличает ноль от «не
измерялось». Миграция, добавляющая такую колонку, не подставляет ноль
историческим строкам: ноль означает «проверено, пусто», а не «не знаем», и
подстановка выдаёт неизмеренное за измеренное — с видом измерения. Пример:
delivery.skipped_entities, по которому ретеншен решает, можно ли удалить тело. - Метка изменения строки меняется только при изменении содержимого. Апдейт,
трогающий одни метаданные (провенанс, ссылки),
updated_atне двигает — иначе она становится меткой касания, и запрос «что изменилось с момента X» получает столько ложных изменений, сколько раз источник переприслал то же самое (у тренировки — двадцать шесть). - Новая производная от разбора колонка в момент появления вносится в перечень того, что пересборка не переносит. Перечень — единственное место, где это сказано, и следующий автор решает по нему; поле, не внесённое туда, однажды перенесут «для полноты учёта», и витрина снова станет функцией предыдущего прогона.
- Временные метки —
TEXTв RFC 3339, UTC, суффиксZ. Фиксированная ширина сохраняет лексикографическую сортировку = хронологию. Единая точка генерации —store.Now(), а не дефолт в схеме: забытая вставка должна падать громко. - Enum-поля — обычный
TEXTбезCHECK, допустимые значения держит код. - Миграции — goose (
internal/store/migrations), SQL для DDL. При изменении структуры обновляем схему в database.md тем же изменением — это проверяетtask gate.