Files
healthlog/docs/database.md
T
av 34e5109b6d непокрытые секции доставки видны в статусе разбора
- половина потока (50 доставок из 104) не несёт metrics вовсе и до сих пор
  числилась parsed: ретеншен, поверив статусу, срезал бы тела stateOfMind,
  которых в экспорте Apple нет
- разбор перечисляет верхнеуровневые ключи data, непокрытые проглатываются
  декодированием: тело 40 МиБ из непокрытой секции удерживает 0 МиБ
- статус partial и колонка delivery.uncovered_sections; миграция переводит
  прежние parsed в pending — им верить нельзя
- витрина не изменилась: отпечаток совпал с прогоном до изменения
2026-08-01 21:25:59 +03:00

8.2 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       │          └──────────────────────────────┘
│ uncovered_sections 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 / partial / failed. Код ответа приёма от него не зависит: сохранили — значит приняли. pending означает «ЭТИМ разбором ещё не смотрели», а не «тела не касались»: миграция 00005 перевела сюда доставки, разобранные кодом, который частичного разбора не различал
points сколько точек дал разбор
headers все заголовки запроса JSON-объектом, кроме несущих секреты
uncovered_sections секции тела, которых разбор не покрыл, JSON-массивом имён; пустой список — []. Ответ на вопрос «что останется потерянным, если тело удалить»: для stateOfMind он необратим, в экспорте Apple секции нет. Ретеншен обязан спрашивать его прежде, чем срезать тело
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 в ключ не входит: он нестабилен и переписывается задним числом. При столкновении выигрывает более полная точка, а не последняя пришедшая.