v4 reflects the live schema (2026-06-20) + the T1.5 m2 column.
The T1.5 migration (20260620000002_add_m2_column_to_calibration_tables.sql) added the Welford M2 accumulator to both swimmer_suits and swimmer_starts. Without M2, the engine cannot do O(1) online Welford updates; recomputing from the full recorded_times history on every save is O(n) per write and loses precision versus the canonical incremental formula.
For each (swimmer, X, distance, stroke) row, the engine maintains:
count (already have sample_count)mean (already have mean_time_ms)M2 (the new column — sum of squared deviations from the mean)stddev (cached in stddev_time_ms, derived from M2 each update)The Welford online update formula is the canonical way to maintain running mean and variance in O(1) per observation:
count_new = count_old + 1
delta = x_new - mean_old
mean_new = mean_old + delta / count_new
M2_new = M2_old + delta * (x_new - mean_new)
variance = M2 / (count - 1)
stddev = sqrt(variance) # cached into stddev_time_ms
Without M2 stored, the engine cannot do incremental updates. It would have to recompute mean and variance from the full history of recorded_times for that (swimmer, X, distance, stroke) bucket on every save — O(n) per write, with precision drift as the sum gets large. The standard Welford is the answer; M2 is the state we need to keep.
The column is hidden (engine-only). The coach never sees m2 in the UI. The engine reads/writes m2 on every recorded_times insert for the matching bucket. stddev_time_ms stays in the table as a cached value (derived from m2 on each update) for backwards compat with the live swimmer_gears shape.
When I tried to apply the v2 migration, it failed with:
ERROR: 42883: operator does not exist: uuid = text
HINT: No operator matches the given name and argument types.
You might need to add explicit type casts.
The error came from the CREATE POLICY in v2's RLS, which used coach_id = auth.uid()::text. The live swimmers.coach_id is uuid (FK to coaches.id), not text. The migration file I read (20260423000000_rls_policies.sql) had the obsolete text-cast pattern; the live DB had been migrated past it. The v3 plan and v3 migration file corrected this by using the live 2-level join pattern (swimmer_id → swimmers.coach_id → coaches.auth_id = auth.uid()). T1 shipped on 2026-06-20.
Lesson (general, not just for Hydrolyze): before drafting a migration, query the live schema (information_schema.columns, pg_constraint, pg_policies) and read the actual state. Migration files are design-time documents; the live DB is the implementation that counts.
workout_sets:
suit boolean NOT NULL DEFAULT falsestart_type text NOT NULL DEFAULT 'in_water'CHECK (start_type IN ('in_water', 'dive', 'backstroke_start'))
swimmer_suits (matches the live swimmer_gears shape + v4 m2):
id uuid NOT NULL DEFAULT gen_random_uuid() — PKswimmer_id uuid NOT NULL REFERENCES swimmers(id) ON DELETE CASCADEsuit boolean NOT NULLdistance integer NOT NULLstroke text NOT NULLmean_time_ms double precision — nullablestddev_time_ms double precision — nullablem2 double precision NOT NULL DEFAULT 0 — Welford accumulator (T1.5, 2026-06-20)sample_count integer NOT NULL DEFAULT 0updated_at timestamptz NOT NULL DEFAULT now()swimmer_starts (matches swimmer_gears + v4 m2):
start_type text NOT NULL replacing suit booleanRLS (live pattern, both tables):
CREATE POLICY "coach_own_swimmer_suits" ON swimmer_suits
FOR ALL USING (
swimmer_id IN (
SELECT id FROM swimmers
WHERE coach_id IN (
SELECT id FROM coaches
WHERE auth_id = auth.uid()
)
)
);
HistoricalTimePredictor.predict() becomes a 4-way match (swimmer, stroke, distance, intensity) + 2 calibration multipliers:
predicted = PB * intensity_mult * suit_mult * start_mult
sigma = max(sampleStdDev(last5), sigmaFloor) + suit_uncertainty + start_uncertainty
Where:
suit_mult = mean(suit=true at distance/stroke) / mean(suit=false at distance/stroke) — read from swimmer_suits joined to swimmer_id and (distance, stroke).start_mult = mean(dive at distance/stroke) / mean(in_water at distance/stroke) — read from swimmer_starts. The in_water row is the reference.The Welford write path (T6, T7 in the v3 plan) updates the tables on every recorded_times insert using the formula above. The m2 column is the source of truth for variance; stddev_time_ms is the cached value. Without m2, the engine would have to recompute the mean and stddev from full history on every save — O(n) per write and losing precision.
Per the existing people/cameron-mcevoy page:
post-2010 FINA rules, no polyurethane). His competition PB is 20.88 in textile. The Cielo record (20.91) was in a tech suit; the difference is ~0.15% — at the limit of detectability from a single race. So for sprint textile racing today, the suit factor is small.
interview). Hydrolyze's coach-cohort won't. The training band for an unsuit-trained swimmer is shifted relative to McEvoy's suit-trained band. The prediction engine has to handle both.
a 5.32 m/s best (consistent 5.2). His 10m time dropped from 3.4s to sub-3.1s. The dive is a real, trainable, measurable component.
text-cast pattern. Never applied.
set-level only. RLS still inherited the text-cast pattern. The migration file 20260620000000_suit_and_dive_adjustment.sql is preserved as an OBSOLETE stub.
drift discovered; live schema is per-(swimmer, X, distance, stroke) stat (not Welford), with 2-level join RLS. T1 shipped with the corrected schema.
Welford updates. The schema is now complete: the engine has all the state it needs to do O(1) incremental updates on every recorded_times insert.
the calibration is a precondition for
calibration that this sits alongside
design evolution with the schema-drift discovery
brainstorm (binary suit, 3-value start; superseded by v4)
brainstorm (4-value enum; superseded)
brainstorm (original)
sibling intensity calibration
supabase/migrations/20260620000000_suit_and_dive_adjustment.sql— the v2 file (OBSOLETE stub, no-op)
supabase/migrations/20260620000001_suit_and_dive_adjustment_v3.sql— the v3 file (T1, shipped 2026-06-20)
supabase/migrations/20260620000002_add_m2_column_to_calibration_tables.sql— the T1.5 file (m2 column, shipped 2026-06-20)
ios-native/Hydrolyze/Sources/Services/HistoricalTimePredictor.swift— the engine file to extend
ios-native/Hydrolyze/Sources/Services/CalibrationFactors.swift— to be created in T2
supabase/migrations/20260423000000_rls_policies.sql — theSTALE migration file (obsolete text-cast pattern); do not trust for live-schema work
Published and managed by TARS, an AI co-author built on Nathan's gbrain.