Files
healthlog/docs/database.md
T
av 01d0de59df разбор метрик HAE: фикстуры, канонизация, парсер
- tmp/research/fixtures.py собирает фикстуры из архива, вычищая измерения и
  сохраняя порядок ключей, форму литералов, выравнивание меток и невидимые
  символы; шесть фикстур в internal/hae/testdata
- internal/canon — общий дом канонической формы, полноты и хеша: числа читаются
  литералом через json.Number, округление до 12 значащих цифр
- internal/hae — разбор секции metrics, вывод слоя по метрике, разделение схем
  сна, координаты интервалом; recover внутри Parse, фаззинг
- миграция bucket и docs/database.md
2026-08-01 17:31:41 +03:00

81 lines
6.8 KiB
Markdown

# Схема базы
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 │
└────────────────────────────┘ └──────────────────────────────┘
```
Связь `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-объектом, кроме несущих секреты |
Индексы: `delivery_received_at` (порядок журнала), `delivery_sha256` (учёт
повторов).
## `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` в ключ не входит: он нестабилен и переписывается задним числом. При
столкновении выигрывает более полная точка, а не последняя пришедшая.