feat: разбор и хранение тренировок и состояния разума
- секции `workouts` и `stateOfMind` покрыты разбором: тренировка лежит одной строкой вместе с маршрутом и внутренними рядами, запись — по ключу `род + id`; миграция 00007 заводит обе таблицы и возвращает в очередь `partial`-доставки с этими ключами - сущность заменяется целиком, но условно: приехавшая побеждает, если не теряет содержания сохранённой (множество ключей и длины верхнеуровневых массивов), а при равном содержании выигрывает версия из более поздней доставки ЖУРНАЛА — «побеждает приехавшая» было бы функцией порядка свёртки, и живая витрина расходилась бы с пересборкой молча - отпечаток витрины покрывает тренировки и записи и снимается одним снимком базы; отчёт `reindex` считает «было и стало» по каждой единице хранения
This commit is contained in:
@@ -29,6 +29,22 @@ SQLite (`modernc.org/sqlite`, чистый Go), миграции — goose, фа
|
||||
│ derived_layer TEXT │ └──────────────────────────────┘
|
||||
│ uncovered_sections TEXT │
|
||||
└────────────────────────────┘
|
||||
┊ ┌──────────────────────────┐ ┌──────────────────────────┐
|
||||
┊ │ workout │ │ record │
|
||||
┊ доставка, │ ─────────────────────── │ │ ─────────────────────── │
|
||||
└┄┄┄ чья версия ┄┄┄┄▶ │ id TEXT PK│ │ kind TEXT ┐ │
|
||||
лежит сейчас │ name TEXT │ │ id TEXT ┘PK│
|
||||
│ start_utc TEXT │ │ ts_utc TEXT │
|
||||
│ end_utc TEXT │ │ tz_offset INTEGER│
|
||||
│ tz_offset INTEGER│ │ payload BLOB │
|
||||
│ duration_sec REAL? │ │ content_hash TEXT │
|
||||
│ payload BLOB │ │ delivery_id TEXT │
|
||||
│ content_hash TEXT │ │ delivery_received_at TEXT│
|
||||
│ delivery_id TEXT │ │ created_at TEXT │
|
||||
│ delivery_received_at TEXT│ │ updated_at TEXT │
|
||||
│ created_at TEXT │ └──────────────────────────┘
|
||||
│ updated_at TEXT │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
Связь `bucket.first_delivery_id → delivery.id` **внешним ключом не объявлена**
|
||||
@@ -89,3 +105,41 @@ SQLite (`modernc.org/sqlite`, чистый Go), миграции — goose, фа
|
||||
`метрика + слой + начало + конец`, у точки-измерения конец равен началу.
|
||||
`source` в ключ не входит: он нестабилен и переписывается задним числом. При
|
||||
столкновении выигрывает более полная точка, а не последняя пришедшая.
|
||||
|
||||
## `workout` и `record` — сущности с собственным `id`
|
||||
|
||||
Вторая единица хранения витрины. Часовой объект им не подходит: у них есть
|
||||
естественный ключ, они редки (за двое суток потока — две тренировки и две
|
||||
записи состояния разума при 44 и 52 доставленных копиях), и группировать их по
|
||||
часам незачем.
|
||||
|
||||
Таблицы две, а не одна с колонкой рода: у тренировки есть заголовок, по
|
||||
которому идёт выборка (имя, интервал, длительность), а у записи его нет. Общая
|
||||
таблица либо теряла бы заголовок, либо держала колонки, пустые у пяти родов из
|
||||
шести.
|
||||
|
||||
| Колонка | Смысл |
|
||||
|---|---|
|
||||
| `workout.id` | идентификатор из HealthKit. Приходит из тела и ограничен по длине разбором: уезжает и в ключ, и в записи лога |
|
||||
| `record.kind` + `record.id` | ключ — **пара**. Собственный `id` наблюдался живьём только у `stateOfMind`, где он UUID; форма идентификатора остальных пяти секций не наблюдалась никем, и короткий несквозной `id` в двух разных секциях затёр бы одну запись другой молча |
|
||||
| `kind` | верхнеуровневый ключ секции HAE **дословно** (`stateOfMind`, не `state_of_mind`): инвариант «форма Apple не транслируется» относится и к именам секций |
|
||||
| `start_utc` / `end_utc` / `ts_utc` | UTC RFC 3339. Конец, которого нет или который не читается, равен началу: ключ — `id`, схлопывать координаты нечем, а истина остаётся в `payload` |
|
||||
| `tz_offset` | смещение зоны **начала**. У `stateOfMind` всегда `0` — это значит «источник прислал UTC», а не «человек был в Гринвиче»: местной зоны у секции в потоке нет вовсе. Клиент, считающий по нему местные сутки, ошибётся |
|
||||
| `duration_sec` | длительность тренировки в секундах, как прислал HAE. **`NULL` означает «источник не прислал»**: ноль — законная длительность. Не вычисляется из интервала — HAE шлёт 91.746 при интервале в 91 секунду |
|
||||
| `payload` | сущность целиком исходными байтами, gzip: заголовок, маршрут, внутренние ряды и сводки. Маршрут — 95% веса тренировки, а такой JSON жмётся примерно в 25 раз. Внутрь SQL-функциями не заглянуть — та же плата, что у `bucket.payload` |
|
||||
| `content_hash` | хеш канонической формы: детектор изменений, не ключ. Тренировка переприсылается каждой доставкой, пока не доедет маршрут (44 копии дают три различных содержимых) |
|
||||
| `delivery_id`, `delivery_received_at` | провенанс: доставка, **чья версия лежит сейчас**, и её метка приёма. Не отчётность: по паре разрешается тай-брейк между версиями равной полноты |
|
||||
|
||||
Индексы: `workout_start_utc` («заголовки тренировок за период» — основной запрос
|
||||
трекера), `record_kind_ts` («записи такого-то рода за период» — единственная
|
||||
форма запроса к таблице).
|
||||
|
||||
**Ряд пульса внутри тренировки лежит в её `payload`, а не в объектах метрики
|
||||
`heart_rate`.** Пульс приезжает дважды — в общем потоке и внутри тренировки; это
|
||||
разные таблицы, и смешение задвоило бы ряд.
|
||||
|
||||
**Замена версии условна.** Приехавшая побеждает, если не теряет содержания
|
||||
сохранённой (множество ключей с непустым значением плюс длины верхнеуровневых
|
||||
массивов); при равных наборах выигрывает версия из более поздней доставки
|
||||
журнала, а не свёрнутая последней. Подробности и обоснование — в
|
||||
`architecture.md`, раздел «Тренировки и прочие секции».
|
||||
|
||||
Reference in New Issue
Block a user