- internal/fold — свёртка по идентификатору доставки, тело из архива: тот же код, каким пойдёт пересборка витрины - приём зовёт свёртку на context.WithoutCancel с собственным дедлайном; исход разбора на код ответа не влияет - слой доставки хранится в delivery.derived_layer и наследуется от ПРЕДШЕСТВУЮЩЕЙ доставки автоматизации: без границы по времени свёртка переставала быть функцией от префикса журнала (1737 объектов против 1742) - task verify:archive — сходимость на живом архиве, 99 доставок из 99
83 lines
7.4 KiB
Markdown
83 lines
7.4 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 │
|
|
│ 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` в ключ не входит: он нестабилен и переписывается задним числом. При
|
|
столкновении выигрывает более полная точка, а не последняя пришедшая.
|