feat: разбор и хранение тренировок и состояния разума
- секции `workouts` и `stateOfMind` покрыты разбором: тренировка лежит одной строкой вместе с маршрутом и внутренними рядами, запись — по ключу `род + id`; миграция 00007 заводит обе таблицы и возвращает в очередь `partial`-доставки с этими ключами - сущность заменяется целиком, но условно: приехавшая побеждает, если не теряет содержания сохранённой (множество ключей и длины верхнеуровневых массивов), а при равном содержании выигрывает версия из более поздней доставки ЖУРНАЛА — «побеждает приехавшая» было бы функцией порядка свёртки, и живая витрина расходилась бы с пересборкой молча - отпечаток витрины покрывает тренировки и записи и снимается одним снимком базы; отчёт `reindex` считает «было и стало» по каждой единице хранения
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
-- +goose Up
|
||||
-- Вторая единица хранения витрины: сущность с собственным идентификатором.
|
||||
-- Часовой объект ей не подходит — у неё есть естественный ключ, она редка (за
|
||||
-- двое суток потока две тренировки и две записи состояния разума при 44 и 52
|
||||
-- доставленных копиях), и группировать её по часам незачем.
|
||||
--
|
||||
-- Таблицы две, а не одна с колонкой рода: у тренировки есть заголовок, по
|
||||
-- которому идёт выборка (имя, интервал, длительность), а у записи его нет.
|
||||
-- Общая таблица либо теряла бы заголовок, либо держала колонки, пустые у пяти
|
||||
-- родов из шести.
|
||||
CREATE TABLE workout (
|
||||
-- Идентификатор из HealthKit. Приходит из тела и ограничен по длине
|
||||
-- разбором: уезжает и в ключ, и в записи лога.
|
||||
id TEXT PRIMARY KEY,
|
||||
|
||||
-- Имя как прислал HAE, локализованное («В помещении Ходьба» — машинная
|
||||
-- калька с Indoor Walk). Хранится дословно; стабильный код HealthKit
|
||||
-- припишет задача словаря категориальных значений.
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- Интервал в UTC, RFC 3339. Конец, которого нет или который не читается,
|
||||
-- равен началу: ключ — id, схлопывать координаты нечем, а истина остаётся
|
||||
-- в payload.
|
||||
start_utc TEXT NOT NULL,
|
||||
end_utc TEXT NOT NULL,
|
||||
-- Смещение зоны НАЧАЛА: колонка одна, а тренировка через смену зоны дала
|
||||
-- бы два разных.
|
||||
tz_offset INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Длительность в секундах, как прислал HAE. NULL означает «источник не
|
||||
-- прислал»: ноль — законная длительность, и потребитель, сложивший
|
||||
-- столбец, иначе не отличил бы одно от другого. Не вычисляется из
|
||||
-- интервала — HAE шлёт 91.746 при интервале в 91 секунду.
|
||||
duration_sec REAL,
|
||||
|
||||
-- Тренировка целиком исходными байтами, сжатая gzip: заголовок, маршрут,
|
||||
-- внутренние ряды и сводки. Маршрут — 95% веса (190 КБ из 199,6 у
|
||||
-- десятиминутной прогулки), а такой JSON жмётся примерно в 25 раз.
|
||||
-- Внутрь средствами SQL не заглянуть — та же плата, что у bucket.payload.
|
||||
payload BLOB NOT NULL,
|
||||
-- Хеш канонической формы содержимого: детектор изменений, а не ключ.
|
||||
-- Тренировка переприсылается каждой доставкой, пока не доедет маршрут: 44
|
||||
-- копии на живом архиве дают три различных содержимых.
|
||||
content_hash TEXT NOT NULL,
|
||||
|
||||
-- Провенанс: доставка, ЧЬЯ ВЕРСИЯ лежит сейчас, и её метка приёма. Не
|
||||
-- отчётность: по паре (received_at, id) разрешается тай-брейк между
|
||||
-- версиями равной полноты. «Побеждает приехавшая» было бы функцией порядка
|
||||
-- свёртки, а он порядку журнала не равен — доставка с более ранней меткой,
|
||||
-- свёрнутая позже, вернула бы витрину к недосчитанной версии, и пересборка
|
||||
-- разошлась бы с живым приёмом молча.
|
||||
-- Без DEFAULT намеренно: единственный писатель заполняет обе колонки
|
||||
-- всегда, а умолчание превратило бы его дефект из отказа вставки в тихо
|
||||
-- неверный исход — строка с пустой меткой оказалась бы «самой ранней в
|
||||
-- журнале», и её затирала бы любая приехавшая версия.
|
||||
delivery_id TEXT NOT NULL,
|
||||
delivery_received_at TEXT NOT NULL,
|
||||
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Основной запрос трекера — «заголовки тренировок за период».
|
||||
CREATE INDEX workout_start_utc ON workout (start_utc);
|
||||
|
||||
CREATE TABLE record (
|
||||
-- Род — верхнеуровневый ключ секции HAE ДОСЛОВНО (`stateOfMind`, не
|
||||
-- `state_of_mind`): инвариант «форма Apple не транслируется» относится и к
|
||||
-- именам секций.
|
||||
kind TEXT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
|
||||
-- Метка события в UTC и смещение исходной зоны. У stateOfMind HAE шлёт
|
||||
-- RFC 3339 в UTC, поэтому смещение там всегда 0 — это значит «источник
|
||||
-- прислал UTC», а не «человек был в Гринвиче»: местной зоны у секции в
|
||||
-- потоке нет вовсе.
|
||||
ts_utc TEXT NOT NULL,
|
||||
tz_offset INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
payload BLOB NOT NULL,
|
||||
content_hash TEXT NOT NULL,
|
||||
|
||||
delivery_id TEXT NOT NULL,
|
||||
delivery_received_at TEXT NOT NULL,
|
||||
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
|
||||
-- Ключ — ПАРА, а не один id. Собственный id наблюдался живьём только у
|
||||
-- stateOfMind, где он UUID HealthKit; форма идентификатора остальных пяти
|
||||
-- секций не наблюдалась никем, и короткий несквозной id в двух разных
|
||||
-- секциях затёр бы одну запись другой молча. Пара стоит ноль: запросы к
|
||||
-- записям всегда идут с родом.
|
||||
PRIMARY KEY (kind, id)
|
||||
);
|
||||
|
||||
-- «Записи такого-то рода за период» — единственная форма запроса к таблице.
|
||||
CREATE INDEX record_kind_ts ON record (kind, ts_utc);
|
||||
|
||||
-- Покрыли секцию — пересверните. Список непокрытых ключей это снимок покрытия
|
||||
-- на момент свёртки: доставки, свёрнутые до того, как workouts и stateOfMind
|
||||
-- стали покрытыми, остались бы partial со старым списком, и ретеншен вечно
|
||||
-- щадил бы тела, которые больше ничего не хранят сверх витрины.
|
||||
--
|
||||
-- Отбор по ЭЛЕМЕНТУ массива, а не по подстроке тела: имя секции приходит из
|
||||
-- чужого тела, и LIKE '%workouts%' поймал бы ключ, лишь содержащий эту
|
||||
-- подстроку. Перевод точечный, а не «все partial»: доставка с непокрытой ecg
|
||||
-- пересворачивать нечего.
|
||||
UPDATE delivery
|
||||
SET parse_status = 'pending'
|
||||
WHERE parse_status = 'partial'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM json_each(delivery.uncovered_sections)
|
||||
WHERE json_each.value IN ('workouts', 'stateOfMind')
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
-- Строки, переведённые в pending, Down обратно не возвращает: какими они были,
|
||||
-- восстановить неоткуда, а pending консервативен — ретеншен его не трогает.
|
||||
DROP INDEX record_kind_ts;
|
||||
DROP TABLE record;
|
||||
DROP INDEX workout_start_utc;
|
||||
DROP TABLE workout;
|
||||
Reference in New Issue
Block a user