mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
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.
96 lines
4.2 KiB
SQL
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
|
|
$$;
|