Files
healthlog/docs/database.md
T
av 32f21044e4 свёртка доставки в объекты и сшивка с приёмом
- internal/fold — свёртка по идентификатору доставки, тело из архива: тот же
  код, каким пойдёт пересборка витрины
- приём зовёт свёртку на context.WithoutCancel с собственным дедлайном;
  исход разбора на код ответа не влияет
- слой доставки хранится в delivery.derived_layer и наследуется от
  ПРЕДШЕСТВУЮЩЕЙ доставки автоматизации: без границы по времени свёртка
  переставала быть функцией от префикса журнала (1737 объектов против 1742)
- task verify:archive — сходимость на живом архиве, 99 доставок из 99
2026-08-01 18:03:59 +03:00

7.4 KiB

Схема базы

SQLite (modernc.org/sqlite, чистый Go), миграции — goose, файлы в internal/store/migrations. Время в текстовых колонках всегда UTC RFC 3339 с секундной точностью: фиксированная ширина делает лексикографическую сортировку TEXT совпадающей с хронологией.

База производна. Источник истины — сырой архив тел (data/raw); состояние пересобирается свёрткой import(экспорт Apple) + replay(доставки по received_at). Поэтому терять базу неприятно, но не смертельно, а вот терять архив — смертельно.

┌────────────────────────────┐          ┌──────────────────────────────┐
│ delivery                   │          │ bucket                       │
│ ─────────────────────────  │          │ ────────────────────────────  │
│ id              TEXT PK    │          │ metric            TEXT ┐      │
│ received_at     TEXT       │          │ layer             TEXT ├ PK   │
│ automation_name TEXT       │  ┄┄┄┄┄▶  │ hour_utc          TEXT ┘      │
│ automation_id   TEXT       │ первая   │ units             TEXT        │
│ aggregation     TEXT       │ доставка │ payload           BLOB        │
│ period          TEXT       │  часа    │ content_hash      TEXT        │
│ session_id      TEXT       │          │ points            INTEGER     │
│ bytes           INTEGER    │          │ first_ts          TEXT        │
│ sha256          TEXT       │          │ last_ts           TEXT        │
│ raw_path        TEXT       │          │ first_delivery_id TEXT        │
│ parse_status    TEXT       │          │ sealed            INTEGER     │
│ points          INTEGER    │          │ created_at        TEXT        │
│ headers         TEXT       │          │ updated_at        TEXT        │
│ derived_layer   TEXT       │          └──────────────────────────────┘
└────────────────────────────┘

Связь bucket.first_delivery_id → delivery.id внешним ключом не объявлена намеренно: доставки подрезаются ретеншеном до следующего проверенного экспорта, а объекты живут дольше. Ссылка — провенанс для разбора слияний, а не целостность.

delivery — учёт принятых пакетов

Одна строка на принятое тело. Само тело лежит в архиве по raw_path (относительный путь внутри data/raw).

Колонка Смысл
id ULID, он же имя файла в архиве
received_at время приёма, UTC
automation_name, automation_id какая автоматизация HAE прислала; automation_id нужен выводу слоя — по нему наследуется слой доставки без плотных метрик
aggregation заголовок automation-aggregation. Режима не означает: значение Default наблюдалось у посекундного, минутного и часового режимов одновременно
period заголовок периода (Since Last Sync и прочие)
bytes, sha256 размер и хеш тела; хеш пока только для учёта
parse_status pending / parsed / failed. Код ответа приёма от него не зависит: сохранили — значит приняли
points сколько точек дал разбор
headers все заголовки запроса JSON-объектом, кроме несущих секреты
derived_layer слой, выведенный для этой доставки. Нужен не отчётности, а самому выводу: доставка без плотных метрик наследует последний надёжно выведенный слой той же автоматизации, и без хранения этой памяти первая такая доставка после перезапуска осталась бы без слоя

Индексы: delivery_received_at (порядок журнала), delivery_sha256 (учёт повторов), delivery_automation_layer (поиск последнего слоя автоматизации).

bucket — часовой объект точек

Единица хранения — час одной метрики в одном слое, а не отдельная точка.

Колонка Смысл
metric имя метрики как прислал HAE. Исключение — sleep_analysis_summary: под именем sleep_analysis приезжают две несовместимые схемы, и они разводятся на разные имена
layer sample / raw / minute / hour / day. Выводится из выравнивания меток
hour_utc начало часа, UTC. Час берётся по началу точки
units единицы метрики. Внутри точки их нет, они живут на уровне метрики
payload точки часа: JSON-массив исходных байтов, gzip. Точки упорядочены по началу
content_hash хеш канонической формы — детектор изменений, не ключ. Совпал — записи нет
points сколько точек внутри
first_ts, last_ts границы содержимого; каталогу разрезов, чтобы не разжимать блоб ради диапазона
first_delivery_id доставка, создавшая объект
sealed час, в который досчёт не ожидается. Правило перевода пока не определено

Таблица WITHOUT ROWID: обращение всегда по полному первичному ключу, и лишний уровень косвенности через rowid ни разу не нужен.

Идентичность точки внутри объекта — координаты метрика + слой + начало + конец, у точки-измерения конец равен началу. source в ключ не входит: он нестабилен и переписывается задним числом. При столкновении выигрывает более полная точка, а не последняя пришедшая.