-- +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;
