Files
HealthLog/prisma/migrations/0105_v1100_categorical_event_rows/migration.sql
T
Marc-André Bombeck b06b1cada4 feat(healthkit): add a categorical device-event row class
Add a discrete EVENT row class to the Measurement model for the
device-flagged health notifications a wearable produces on its own —
irregular-rhythm, high/low heart rate, walking-steadiness and
breathing-disturbance alerts.

Following the sleep-stage / glucose-context nullable-context precedent,
this is an additive nullable column on the existing table, not a new
storage paradigm. Five new MeasurementType values carry the event class
and a new rhythm_classification enum column carries the device's own
verdict / severity (migration 0105, with a CHECK that pins the column to
the event types). An event row stores value 1 and the device verdict;
it never participates in trend, rollup, chart or personal-record
analytics.

For the irregular-rhythm class, HealthLog ingests only the
classification RESULT the device's own certified on-device algorithm
already produced. It never ingests a raw ECG waveform, never
re-classifies, and never derives a verdict of its own.

The Apple Health batch ingest maps the five category identifiers, with
the device's HKCategoryValue codepoint resolved to the stored
classification; an unknown codepoint degrades to a fallback verdict
rather than dropping the event.
2026-06-02 22:07:28 +02:00

96 lines
4.2 KiB
SQL

-- v1.10.0 — categorical EVENT row class + device rhythm classification.
--
-- Adds a discrete EVENT row class to the existing `Measurement` model,
-- following the `sleep_stage` / `glucose_context` nullable-context
-- precedent (migration 0021 + 0036) — an additive nullable column on the
-- existing table, NOT a new storage paradigm. An EVENT row stores ONE
-- on-device notification the user's wearable already produced (Apple Watch
-- irregular-rhythm / high-HR / low-HR / walking-steadiness, ScanWatch AFib
-- screening, sleep breathing-disturbance). `value` is always 1; the
-- device's own verdict / severity rides in the new `rhythm_classification`
-- column.
--
-- ── Regulatory framing (load-bearing) ──────────────────────────────────
-- HealthLog stores ONLY the classification RESULT the device's
-- FDA-cleared / CE-marked on-device algorithm emitted. It never ingests a
-- raw ECG waveform, never re-classifies, and never produces a HealthLog
-- diagnosis. The surface is awareness/screening of the DEVICE's decision.
--
-- Two schema objects:
--
-- 1. Five new `measurement_type` enum values (the EVENT classes).
-- 2. A new `rhythm_classification` enum + a nullable column carrying the
-- device verdict / severity, NULL for every continuous measurement.
--
-- Idempotent guards (`IF NOT EXISTS`) make reruns safe. Forward-only.
--
-- Reversibility:
-- ALTER TABLE "measurements" DROP CONSTRAINT IF EXISTS "measurements_rhythm_classification_requires_event_type";
-- ALTER TABLE "measurements" DROP COLUMN IF EXISTS "rhythm_classification";
-- DROP TYPE IF EXISTS "rhythm_classification";
-- Postgres does not support removing an enum value, so the five new
-- `measurement_type` members stay; with no rows carrying them after a
-- column drop they are inert.
-- ── 1. measurement_type — append the five EVENT classes ────────────────
-- Pure additive; no existing row carries any of these values. The
-- `IF NOT EXISTS` guard keeps the migration idempotent across a
-- partially-applied environment.
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'IRREGULAR_RHYTHM_NOTIFICATION';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'HIGH_HEART_RATE_EVENT';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'LOW_HEART_RATE_EVENT';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'WALKING_STEADINESS_EVENT';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'BREATHING_DISTURBANCE_EVENT';
-- ── 2. rhythm_classification — the device verdict / severity enum ──────
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'rhythm_classification') THEN
CREATE TYPE "rhythm_classification" AS ENUM (
'IRREGULAR',
'NOT_DETECTED',
'INCONCLUSIVE',
'LOW',
'VERY_LOW',
'FIRED'
);
END IF;
END
$$;
-- Nullable column. NULL for every continuous measurement; non-NULL only
-- for the EVENT classes. Mirrors the `sleep_stage` additive-column shape.
ALTER TABLE "measurements"
ADD COLUMN IF NOT EXISTS "rhythm_classification" "rhythm_classification";
-- Enforce: rhythm_classification is only set for the EVENT-class rows.
-- Mirrors the `measurements_sleep_stage_requires_type` CHECK (migration
-- 0036) and the glucose_context CHECK (migration 0021).
--
-- The comparison casts `type` to text so the predicate references the
-- newly-added enum members as string literals. Postgres forbids using an
-- enum value in the same transaction that adds it (the ALTER TYPE ... ADD
-- VALUE statements above run in this same migration transaction); the
-- `::text` cast sidesteps that restriction without weakening the check.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'measurements_rhythm_classification_requires_event_type'
) THEN
ALTER TABLE "measurements"
ADD CONSTRAINT "measurements_rhythm_classification_requires_event_type"
CHECK (
"rhythm_classification" IS NULL
OR "type"::text IN (
'IRREGULAR_RHYTHM_NOTIFICATION',
'HIGH_HEART_RATE_EVENT',
'LOW_HEART_RATE_EVENT',
'WALKING_STEADINESS_EVENT',
'BREATHING_DISTURBANCE_EVENT'
)
);
END IF;
END
$$;