From 37ea1f85378a6300b90e3cf6ec6b23e9b9408ea9 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 27 Mar 2026 21:23:56 +0100 Subject: [PATCH] fix: vitals_baseline dynamic query parameter mismatch **Bug:** POST /api/vitals/baseline threw UndefinedParameter **Cause:** Dynamic SQL generation had desynchronized column names and placeholders **Fix:** Rewrote to use synchronized insert_cols, insert_placeholders, update_fields arrays - Track param_idx correctly (start at 3 after pid and date) - Build INSERT columns and placeholders in parallel - Cleaner, more maintainable code - Fixes Ruhepuls entry error --- backend/routers/vitals_baseline.py | 67 ++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/backend/routers/vitals_baseline.py b/backend/routers/vitals_baseline.py index cd460ee..39aae6c 100644 --- a/backend/routers/vitals_baseline.py +++ b/backend/routers/vitals_baseline.py @@ -99,52 +99,83 @@ def create_or_update_baseline( """Create or update baseline entry (upsert on date).""" pid = get_pid(x_profile_id) - # Build dynamic update columns (only non-None fields) - fields = [] + # Build dynamic INSERT columns and UPDATE fields + insert_cols = [] + insert_placeholders = [] + update_fields = [] values = [pid, entry.date] + param_idx = 3 # Start after $1 (pid) and $2 (date) if entry.resting_hr is not None: - fields.append("resting_hr = COALESCE(EXCLUDED.resting_hr, vitals_baseline.resting_hr)") + insert_cols.append("resting_hr") + insert_placeholders.append(f"${param_idx}") + update_fields.append(f"resting_hr = COALESCE(EXCLUDED.resting_hr, vitals_baseline.resting_hr)") values.append(entry.resting_hr) + param_idx += 1 + if entry.hrv is not None: - fields.append("hrv = COALESCE(EXCLUDED.hrv, vitals_baseline.hrv)") + insert_cols.append("hrv") + insert_placeholders.append(f"${param_idx}") + update_fields.append(f"hrv = COALESCE(EXCLUDED.hrv, vitals_baseline.hrv)") values.append(entry.hrv) + param_idx += 1 + if entry.vo2_max is not None: - fields.append("vo2_max = COALESCE(EXCLUDED.vo2_max, vitals_baseline.vo2_max)") + insert_cols.append("vo2_max") + insert_placeholders.append(f"${param_idx}") + update_fields.append(f"vo2_max = COALESCE(EXCLUDED.vo2_max, vitals_baseline.vo2_max)") values.append(entry.vo2_max) + param_idx += 1 + if entry.spo2 is not None: - fields.append("spo2 = COALESCE(EXCLUDED.spo2, vitals_baseline.spo2)") + insert_cols.append("spo2") + insert_placeholders.append(f"${param_idx}") + update_fields.append(f"spo2 = COALESCE(EXCLUDED.spo2, vitals_baseline.spo2)") values.append(entry.spo2) + param_idx += 1 + if entry.respiratory_rate is not None: - fields.append("respiratory_rate = COALESCE(EXCLUDED.respiratory_rate, vitals_baseline.respiratory_rate)") + insert_cols.append("respiratory_rate") + insert_placeholders.append(f"${param_idx}") + update_fields.append(f"respiratory_rate = COALESCE(EXCLUDED.respiratory_rate, vitals_baseline.respiratory_rate)") values.append(entry.respiratory_rate) + param_idx += 1 + if entry.body_temperature is not None: - fields.append("body_temperature = COALESCE(EXCLUDED.body_temperature, vitals_baseline.body_temperature)") + insert_cols.append("body_temperature") + insert_placeholders.append(f"${param_idx}") + update_fields.append(f"body_temperature = COALESCE(EXCLUDED.body_temperature, vitals_baseline.body_temperature)") values.append(entry.body_temperature) + param_idx += 1 + if entry.resting_metabolic_rate is not None: - fields.append("resting_metabolic_rate = COALESCE(EXCLUDED.resting_metabolic_rate, vitals_baseline.resting_metabolic_rate)") + insert_cols.append("resting_metabolic_rate") + insert_placeholders.append(f"${param_idx}") + update_fields.append(f"resting_metabolic_rate = COALESCE(EXCLUDED.resting_metabolic_rate, vitals_baseline.resting_metabolic_rate)") values.append(entry.resting_metabolic_rate) + param_idx += 1 + if entry.note: - fields.append("note = COALESCE(EXCLUDED.note, vitals_baseline.note)") + insert_cols.append("note") + insert_placeholders.append(f"${param_idx}") + update_fields.append(f"note = COALESCE(EXCLUDED.note, vitals_baseline.note)") values.append(entry.note) + param_idx += 1 # At least one field must be provided - if not fields: + if not insert_cols: raise HTTPException(400, "At least one baseline vital must be provided") - # Build value placeholders - placeholders = ", ".join([f"${i}" for i in range(1, len(values) + 1)]) - with get_db() as conn: cur = get_cursor(conn) query = f""" - INSERT INTO vitals_baseline (profile_id, date, {', '.join([f.split('=')[0].strip() for f in fields])}) - VALUES ($1, $2, {', '.join([f'${i}' for i in range(3, len(values) + 1)])}) + INSERT INTO vitals_baseline (profile_id, date, {', '.join(insert_cols)}) + VALUES ($1, $2, {', '.join(insert_placeholders)}) ON CONFLICT (profile_id, date) - DO UPDATE SET {', '.join(fields)}, updated_at = NOW() + DO UPDATE SET {', '.join(update_fields)}, updated_at = NOW() RETURNING * """ - cur.execute(query, values) + cur.execute(query, tuple(values)) return r2d(cur.fetchone())