diff --git a/backend/goal_utils.py b/backend/goal_utils.py index 09bd9cc..421450c 100644 --- a/backend/goal_utils.py +++ b/backend/goal_utils.py @@ -278,7 +278,7 @@ def _fetch_by_aggregation_method( - max_30d: Maximum value in last 30 days Args: - filter_conditions: Optional JSON filters (e.g., {"training_type": "strength"}) + filter_conditions: Optional JSON filters (e.g., {"training_category": "strength"}) """ # Guard: source_table/column required for simple aggregation if not table or not column: @@ -412,7 +412,21 @@ def _fetch_by_aggregation_method( return None except Exception as e: + # Log detailed error for debugging print(f"[ERROR] Failed to fetch value from {table}.{column} using {method}: {e}") + print(f"[ERROR] Filter conditions: {filter_conditions}") + print(f"[ERROR] Filter SQL: {filter_sql}") + print(f"[ERROR] Filter params: {filter_params}") + + # CRITICAL: Rollback transaction to avoid InFailedSqlTransaction errors + try: + conn.rollback() + print(f"[INFO] Transaction rolled back after query error") + except Exception as rollback_err: + print(f"[WARNING] Rollback failed: {rollback_err}") + + # Return None so goal creation can continue without current_value + # (current_value will be NULL in the goal record) return None diff --git a/backend/routers/vitals_baseline.py b/backend/routers/vitals_baseline.py index 39aae6c..8651ef8 100644 --- a/backend/routers/vitals_baseline.py +++ b/backend/routers/vitals_baseline.py @@ -99,67 +99,72 @@ def create_or_update_baseline( """Create or update baseline entry (upsert on date).""" pid = get_pid(x_profile_id) - # Build dynamic INSERT columns and UPDATE fields + # Build dynamic INSERT columns, placeholders, UPDATE fields, and values list + # All arrays must stay synchronized insert_cols = [] insert_placeholders = [] update_fields = [] - values = [pid, entry.date] - param_idx = 3 # Start after $1 (pid) and $2 (date) + param_values = [] # Will contain ALL values including pid and date + + # Always include profile_id and date + param_values.append(pid) + param_values.append(entry.date) + param_idx = 3 # Next parameter starts at $3 if entry.resting_hr is not None: 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) + update_fields.append(f"resting_hr = EXCLUDED.resting_hr") + param_values.append(entry.resting_hr) param_idx += 1 if entry.hrv is not None: 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) + update_fields.append(f"hrv = EXCLUDED.hrv") + param_values.append(entry.hrv) param_idx += 1 if entry.vo2_max is not None: 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) + update_fields.append(f"vo2_max = EXCLUDED.vo2_max") + param_values.append(entry.vo2_max) param_idx += 1 if entry.spo2 is not None: 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) + update_fields.append(f"spo2 = EXCLUDED.spo2") + param_values.append(entry.spo2) param_idx += 1 if entry.respiratory_rate is not None: 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) + update_fields.append(f"respiratory_rate = EXCLUDED.respiratory_rate") + param_values.append(entry.respiratory_rate) param_idx += 1 if entry.body_temperature is not None: 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) + update_fields.append(f"body_temperature = EXCLUDED.body_temperature") + param_values.append(entry.body_temperature) param_idx += 1 if entry.resting_metabolic_rate is not None: 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) + update_fields.append(f"resting_metabolic_rate = EXCLUDED.resting_metabolic_rate") + param_values.append(entry.resting_metabolic_rate) param_idx += 1 if entry.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) + update_fields.append(f"note = EXCLUDED.note") + param_values.append(entry.note) param_idx += 1 # At least one field must be provided @@ -168,14 +173,24 @@ def create_or_update_baseline( with get_db() as conn: cur = get_cursor(conn) + + # Build complete column list and placeholder list + all_cols = f"profile_id, date, {', '.join(insert_cols)}" + all_placeholders = f"$1, $2, {', '.join(insert_placeholders)}" + query = f""" - INSERT INTO vitals_baseline (profile_id, date, {', '.join(insert_cols)}) - VALUES ($1, $2, {', '.join(insert_placeholders)}) + INSERT INTO vitals_baseline ({all_cols}) + VALUES ({all_placeholders}) ON CONFLICT (profile_id, date) DO UPDATE SET {', '.join(update_fields)}, updated_at = NOW() RETURNING * """ - cur.execute(query, tuple(values)) + + # Debug logging + print(f"[DEBUG] Vitals baseline query: {query}") + print(f"[DEBUG] Param values ({len(param_values)}): {param_values}") + + cur.execute(query, tuple(param_values)) return r2d(cur.fetchone())