mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
Indoor workouts had no home for their per-sample heart-rate series: it
rode on WorkoutRoute.sampleTimestamps, which only exists when a workout
carries GPS geometry. A treadmill or trainer session therefore lost the
signal the training-strain reader needs.
Add a WorkoutSamples child (workout_samples), 1:1 with the workout like
WorkoutRoute, holding a JSONB `[{ t, hr?, speedMs?, power?, cadence? }]`
array plus a denormalised sample_count. JSONB keeps the self-hosted
Postgres extension-free, mirroring the route table's decision. Migration
0107.
Also retire the stale cross-source workout-dedup marker on the Workout
model: that work landed as the write-time dedupeWorkoutBatch() picker
plus the read-time pickCanonicalWorkoutRows() picker, both resolving the
user's source-priority ladder. The comment now documents the two layers
instead of tracking them as open.
45 lines
2.2 KiB
SQL
45 lines
2.2 KiB
SQL
-- v1.10.0 — per-workout heart-rate series (route-independent).
|
|
--
|
|
-- A companion to `workout_routes` that holds the per-sample HR series
|
|
-- WITHOUT requiring GPS geometry. The route table only exists when a
|
|
-- workout has a GeoJSON LineString, so an INDOOR session (treadmill,
|
|
-- trainer, strength) had nowhere to store its heart-rate samples even
|
|
-- though HealthKit ships them. That series feeds the training-strain
|
|
-- engine (TRIMP / time-in-HR-zone), so decoupling it from the route is
|
|
-- what unblocks indoor strain.
|
|
--
|
|
-- Shape rationale:
|
|
-- * `samples` JSONB — `[{ t: ISO string, hr?: int, speedMs?, power?,
|
|
-- cadence? }]`, one entry per recorded sample. Same JSONB decision
|
|
-- as `workout_routes.geometry` / `sample_timestamps`: no PostGIS
|
|
-- extension dependency on a self-hosted Postgres install, and the
|
|
-- blob mirrors HealthKit's per-sample shape directly.
|
|
-- * `sample_count` — denormalised `samples.length` so the strain
|
|
-- reader + observability can size the series without deserialising
|
|
-- the JSONB blob. The application caps the array length on write
|
|
-- (MAX_WORKOUT_HR_SAMPLES in src/lib/validations/workout.ts) so the
|
|
-- per-workout payload — and the largest new workout-side write
|
|
-- stream — stays bounded.
|
|
--
|
|
-- 1:1 with the workout: `workout_id` is UNIQUE with an ON DELETE CASCADE
|
|
-- FK, exactly like `workout_routes`. A re-submitted batch that wins the
|
|
-- workout-row race but loses the sample-row race is an idempotent no-op
|
|
-- (the ingest path uses `skipDuplicates`), not a hard error.
|
|
--
|
|
-- Idempotent guards (`IF NOT EXISTS`) make reruns safe. Forward-only.
|
|
--
|
|
-- Reversibility:
|
|
-- DROP TABLE IF EXISTS "workout_samples";
|
|
-- A roll-back loses the captured per-workout HR series; the workout row,
|
|
-- its aggregates (avg/max/min HR), and the route survive untouched.
|
|
|
|
CREATE TABLE IF NOT EXISTS "workout_samples" (
|
|
"id" TEXT PRIMARY KEY,
|
|
"workout_id" TEXT NOT NULL UNIQUE REFERENCES "workouts"("id") ON DELETE CASCADE,
|
|
-- JSONB array: [{ t: ISO string, hr?: int, speedMs?, power?, cadence? }]
|
|
"samples" JSONB NOT NULL,
|
|
-- Denormalised samples.length — sized without parsing the blob.
|
|
"sample_count" INTEGER NOT NULL,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|