From 894ee1dd02ede0503b01416c8890b8d803418598 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 11 Apr 2026 06:27:11 +0200 Subject: [PATCH] refactor(csv_parser): Update training type resolution to use existing database cursor - Modified `_resolve_training_type_for_activity` to accept a database cursor, improving efficiency and avoiding potential deadlocks during CSV imports. - Introduced `get_training_type_for_activity_with_cursor` to handle training type resolution with an existing cursor, streamlining database interactions. - Updated related calls in the activity import logic to utilize the new function, ensuring consistent behavior across the application. --- backend/csv_parser/executor.py | 10 +-- .../051_blood_pressure_source_csv.sql | 9 +++ backend/routers/activity.py | 65 +++++++++++-------- 3 files changed, 52 insertions(+), 32 deletions(-) create mode 100644 backend/migrations/051_blood_pressure_source_csv.sql diff --git a/backend/csv_parser/executor.py b/backend/csv_parser/executor.py index be3d64d..f8025c1 100644 --- a/backend/csv_parser/executor.py +++ b/backend/csv_parser/executor.py @@ -31,11 +31,11 @@ except Exception: # pragma: no cover _EVALUATION_AVAILABLE = False -def _resolve_training_type_for_activity(activity_type: str, profile_id: str): - """Lazy import — ermöglicht Tests ohne Laden von routers.activity (bcrypt).""" - from routers.activity import get_training_type_for_activity +def _resolve_training_type_for_activity(cur, activity_type: str, profile_id: str): + """Lazy import — gleicher DB-Cursor wie der Import (kein verschachteltes get_db / Pool-Deadlock).""" + from routers.activity import get_training_type_for_activity_with_cursor - return get_training_type_for_activity(activity_type, profile_id) + return get_training_type_for_activity_with_cursor(cur, activity_type, profile_id) def coerce_date(val: Any) -> dt.date | None: @@ -873,7 +873,7 @@ def _import_activity( wtype = str(activity_type).strip() training_type_id, training_category, training_subcategory = _resolve_training_type_for_activity( - wtype, profile_id + cur, wtype, profile_id ) iso = date_d.isoformat() diff --git a/backend/migrations/051_blood_pressure_source_csv.sql b/backend/migrations/051_blood_pressure_source_csv.sql new file mode 100644 index 0000000..3999983 --- /dev/null +++ b/backend/migrations/051_blood_pressure_source_csv.sql @@ -0,0 +1,9 @@ +-- Universal-CSV-Import schreibt source = 'csv' (siehe csv_parser/executor _import_blood_pressure). + +ALTER TABLE blood_pressure_log DROP CONSTRAINT IF EXISTS blood_pressure_log_source_check; + +ALTER TABLE blood_pressure_log ADD CONSTRAINT blood_pressure_log_source_check + CHECK (source IN ('manual', 'omron', 'apple_health', 'withings', 'csv')); + +COMMENT ON COLUMN blood_pressure_log.source IS + 'manual | omron | apple_health | withings | csv (Universal-CSV-Import)'; diff --git a/backend/routers/activity.py b/backend/routers/activity.py index b2e2009..40966fc 100644 --- a/backend/routers/activity.py +++ b/backend/routers/activity.py @@ -198,6 +198,43 @@ def activity_stats(x_profile_id: Optional[str]=Header(default=None), session: di return {"count":len(rows),"total_kcal":round(total_kcal),"total_min":round(total_min),"by_type":by_type} +def get_training_type_for_activity_with_cursor(cur, activity_type: str, profile_id: str | None = None): + """ + Wie get_training_type_for_activity, aber mit bestehendem Cursor (z. B. Universal-CSV-Import). + Vermeidet verschachteltes get_db() — bei maxconn=1 sonst Deadlock auf dem Connection-Pool. + """ + if profile_id: + cur.execute( + """ + SELECT m.training_type_id, t.category, t.subcategory + FROM activity_type_mappings m + JOIN training_types t ON m.training_type_id = t.id + WHERE m.activity_type = %s AND m.profile_id = %s + LIMIT 1 + """, + (activity_type, profile_id), + ) + row = cur.fetchone() + if row: + return (row["training_type_id"], row["category"], row["subcategory"]) + + cur.execute( + """ + SELECT m.training_type_id, t.category, t.subcategory + FROM activity_type_mappings m + JOIN training_types t ON m.training_type_id = t.id + WHERE m.activity_type = %s AND m.profile_id IS NULL + LIMIT 1 + """, + (activity_type,), + ) + row = cur.fetchone() + if row: + return (row["training_type_id"], row["category"], row["subcategory"]) + + return (None, None, None) + + def get_training_type_for_activity(activity_type: str, profile_id: str = None): """ Map activity_type to training_type_id using database mappings. @@ -211,33 +248,7 @@ def get_training_type_for_activity(activity_type: str, profile_id: str = None): """ with get_db() as conn: cur = get_cursor(conn) - - # Try user-specific mapping first - if profile_id: - cur.execute(""" - SELECT m.training_type_id, t.category, t.subcategory - FROM activity_type_mappings m - JOIN training_types t ON m.training_type_id = t.id - WHERE m.activity_type = %s AND m.profile_id = %s - LIMIT 1 - """, (activity_type, profile_id)) - row = cur.fetchone() - if row: - return (row['training_type_id'], row['category'], row['subcategory']) - - # Try global mapping - cur.execute(""" - SELECT m.training_type_id, t.category, t.subcategory - FROM activity_type_mappings m - JOIN training_types t ON m.training_type_id = t.id - WHERE m.activity_type = %s AND m.profile_id IS NULL - LIMIT 1 - """, (activity_type,)) - row = cur.fetchone() - if row: - return (row['training_type_id'], row['category'], row['subcategory']) - - return (None, None, None) + return get_training_type_for_activity_with_cursor(cur, activity_type, profile_id) @router.get("/uncategorized")