refactor(csv_parser): Update training type resolution to use existing database cursor
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s

- 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.
This commit is contained in:
Lars 2026-04-11 06:27:11 +02:00
parent a9bd3faabb
commit 894ee1dd02
3 changed files with 52 additions and 32 deletions

View File

@ -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()

View File

@ -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)';

View File

@ -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")