Erste Version Platzhalter EAV #86
|
|
@ -155,6 +155,18 @@ Phasen sind **sequentiell** wo „Abhängigkeit“ steht; Teile können parallel
|
|||
|
||||
**Abhängigkeit:** Phase A + B (sonst Lücken beim Lesen).
|
||||
|
||||
**Analyse (2026-04-16, nur Ist-Review):** Es gibt **keinen aktiven** Schreibpfad mehr, der `activity_log`-Spalten für `source_field`-Parameter **dauerhaft nach EAV spiegelt**.
|
||||
|
||||
| Prüfpunkt | Ergebnis |
|
||||
|-----------|----------|
|
||||
| `sync_column_backed_session_metrics` | Nur noch **Definition** in `activity_session_metrics.py`, als veraltet markiert; **keine Aufrufer** im Repo (grep). Laufzeit-Sync: **abgestellt**. |
|
||||
| `run_activity_post_write_hooks` / `…_import` | Nur **Auto-Eval** (optional); Kommentar: **kein** Spalte→EAV-Sync. |
|
||||
| Universal-CSV (`executor.py`) | Kernfelder → `activity_log` (`activity_csv_registry_updates_from_mapped` + `update_activity_columns` / Insert); EAV → `upsert_session_metrics_from_csv_mapped`. Registry-Keys werden **nicht** nach EAV geschrieben; bei `source_field` wird EAV **übersprungen**, wenn die Spalte **bereits befüllt** ist — vermeidet bewusst doppelte Speicherung. |
|
||||
| REST `PUT /metrics` | Kommentar in Code: **kein** `sync_column_backed` nach EAV-Ersatz. |
|
||||
| Migrationen 055 / 057 | **Einmaliger** Backfill/Schwenk, kein fortlaufender Sync. |
|
||||
|
||||
**Lesepfad (2026-04-16):** `merge_column_backed_and_eav_metrics` bevorzugt **immer** `activity_log`, wenn ein kanonischer Spaltenwert existiert: zuerst `source_field`, dann Registry-Spalte gleichen Keys, dann Legacy-Spalten für EAV-primäre Parameter, zuletzt EAV. Doppelte physische Schreiborte sind damit in der effektiven Sicht **ohne EAV-Vorrang** behoben.
|
||||
|
||||
---
|
||||
|
||||
### Phase D – Composite MVP
|
||||
|
|
@ -200,4 +212,4 @@ Phasen sind **sequentiell** wo „Abhängigkeit“ steht; Teile können parallel
|
|||
|
||||
---
|
||||
|
||||
**Version:** 1.3 · Phase-B-Export an `enrich_sessions_with_metrics` angebunden (`exportdata.py`).
|
||||
**Version:** 1.5 · Merge: activity_log (Registry + Legacy-Spalten) vor EAV bei Lesen.
|
||||
|
|
|
|||
|
|
@ -46,19 +46,19 @@ Schreibpfad: Universal-CSV und API sollen diese Keys auf **`activity_log`** mapp
|
|||
|
||||
`ACTIVITY_EAV_PRIMARY_PARAMETER_KEYS` in `activity_data_canon.py`. **`training_parameters.source_field`** = NULL (nach Kanon / Migration 057): kanonischer Speicher ist **`activity_session_metrics`**.
|
||||
|
||||
| Parameter-Key (`training_parameters.key`) | Legacy-Spalte `activity_log` (nur Lesefallback) | Kanonische Quelle | Lesefallback |
|
||||
|-------------------------------------------|-------------------------------------------------|-------------------|--------------|
|
||||
| `min_hr` | `hr_min` | **EAV** | Spalte, wenn EAV leer |
|
||||
| `pace_min_per_km` | `pace_min_per_km` | **EAV** | Spalte, wenn EAV leer |
|
||||
| `cadence` | `cadence` | **EAV** | Spalte, wenn EAV leer |
|
||||
| `avg_power` | `avg_power` | **EAV** | Spalte, wenn EAV leer |
|
||||
| `elevation_gain` | `elevation_gain` | **EAV** | Spalte, wenn EAV leer |
|
||||
| `temperature_celsius` | `temperature_celsius` | **EAV** | Spalte, wenn EAV leer |
|
||||
| `humidity_percent` | `humidity_percent` | **EAV** | Spalte, wenn EAV leer |
|
||||
| `avg_hr_percent` | `avg_hr_percent` | **EAV** | Spalte, wenn EAV leer |
|
||||
| `kcal_per_km` | `kcal_per_km` | **EAV** | Spalte, wenn EAV leer |
|
||||
| Parameter-Key (`training_parameters.key`) | Legacy-Spalte `activity_log` | Schreib-Kanon (Ziel) |
|
||||
|-------------------------------------------|------------------------------|------------------------|
|
||||
| `min_hr` | `hr_min` | **EAV** |
|
||||
| `pace_min_per_km` | `pace_min_per_km` | **EAV** |
|
||||
| `cadence` | `cadence` | **EAV** |
|
||||
| `avg_power` | `avg_power` | **EAV** |
|
||||
| `elevation_gain` | `elevation_gain` | **EAV** |
|
||||
| `temperature_celsius` | `temperature_celsius` | **EAV** |
|
||||
| `humidity_percent` | `humidity_percent` | **EAV** |
|
||||
| `avg_hr_percent` | `avg_hr_percent` | **EAV** |
|
||||
| `kcal_per_km` | `kcal_per_km` | **EAV** |
|
||||
|
||||
Merge-Implementierung: `merge_column_backed_and_eav_metrics` in `activity_session_metrics.py`.
|
||||
**Lesen:** `merge_column_backed_and_eav_metrics` — wenn Legacy-Spalte **und** EAV einen Wert haben, **gewinnt die Spalte** (kanonische `activity_log`-Sicht). EAV nur, wenn die Spalte leer/nicht koerzierbar ist.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ ACTIVITY_MODULE_REGISTRY_FIELD_KEYS: Final[frozenset[str]] = get_activity_module
|
|||
ACTIVITY_LOG_PATCHABLE_COLUMNS: Final[frozenset[str]] = ACTIVITY_MODULE_REGISTRY_FIELD_KEYS - {"date"}
|
||||
|
||||
# Parameter-Keys (training_parameters.key), die primär in EAV geführt werden; source_field nach Migration 057 NULL.
|
||||
# Lesefallback: activity_log-Spalte unter ACTIVITY_LOG_LEGACY_COLUMN_FOR_EAV_PRIMARY_PARAM, falls EAV leer.
|
||||
# Lesen (Merge): activity_log-Legacy-Spalte schlägt EAV, wenn beide befüllt; sonst EAV.
|
||||
ACTIVITY_EAV_PRIMARY_PARAMETER_KEYS: Final[frozenset[str]] = frozenset(
|
||||
{
|
||||
"min_hr",
|
||||
|
|
@ -46,7 +46,7 @@ ACTIVITY_EAV_PRIMARY_PARAMETER_KEYS: Final[frozenset[str]] = frozenset(
|
|||
}
|
||||
)
|
||||
|
||||
# Spaltenname activity_log für Legacy-Lesefallback (Merge), wenn EAV für den Parameter fehlt.
|
||||
# Spaltenname activity_log für Legacy-Merge (Vorrang vor EAV bei gesetztem Spaltenwert).
|
||||
ACTIVITY_LOG_LEGACY_COLUMN_FOR_EAV_PRIMARY_PARAM: Final[Dict[str, str]] = {
|
||||
"min_hr": "hr_min",
|
||||
"pace_min_per_km": "pace_min_per_km",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Zentrale Persistenz für activity_log + EAV-Nebenwirkungen (Eval, Spalten→EAV).
|
||||
Zentrale Persistenz für activity_log + EAV-Nebenwirkungen (Eval).
|
||||
|
||||
Alle Schreibpfade (REST, Universal-CSV, Legacy-Upload) laufen hier zusammen.
|
||||
|
||||
|
|
|
|||
|
|
@ -323,9 +323,15 @@ def merge_column_backed_and_eav_metrics(
|
|||
"""
|
||||
Effektive Metrikliste **nur** für Parameter aus ``schema`` (Kategorie + Trainingstyp / tcp+ttp).
|
||||
|
||||
Pro Schema-Parameter mit ``source_field`` hat ``activity_log`` Vorrang, wenn die Spalte befüllt und
|
||||
koerzierbar ist; sonst EAV, sonst Legacy-Spalte. EAV-Zeilen zu Parametern, die nicht im Schema sind,
|
||||
werden nicht ausgegeben (Darstellung und Auswertung folgen ausschließlich dem konfigurierten Profil).
|
||||
Kanon beim Lesen: **activity_log** schlägt EAV, sobald ein passender Spaltenwert existiert und
|
||||
koerzierbar ist — in dieser Reihenfolge:
|
||||
|
||||
1. ``source_field`` → Spalte
|
||||
2. Parameter-Key = Registry-Kernfeld (``ACTIVITY_MODULE_REGISTRY_FIELD_KEYS``) → gleichnamige Spalte
|
||||
3. EAV-primäre Keys → Legacy-Spalte laut ``ACTIVITY_LOG_LEGACY_COLUMN_FOR_EAV_PRIMARY_PARAM``
|
||||
4. sonst EAV
|
||||
|
||||
EAV-Zeilen zu Parametern, die nicht im Schema sind, werden nicht ausgegeben.
|
||||
"""
|
||||
eav_by_key = {m["key"]: m for m in eav_metrics}
|
||||
merged: List[Dict[str, Any]] = []
|
||||
|
|
@ -360,10 +366,23 @@ def merge_column_backed_and_eav_metrics(
|
|||
|
||||
if used_column:
|
||||
continue
|
||||
if k in eav_by_key:
|
||||
merged.append(dict(eav_by_key[k]))
|
||||
keys_handled.add(k)
|
||||
continue
|
||||
|
||||
if k in ACTIVITY_MODULE_REGISTRY_FIELD_KEYS and k in header and header[k] is not None:
|
||||
try:
|
||||
val = _coerce_raw_value_for_parameter(dt, header[k])
|
||||
merged.append(
|
||||
{
|
||||
"training_parameter_id": tid,
|
||||
"key": k,
|
||||
"data_type": dt,
|
||||
"unit": unit,
|
||||
"value": val,
|
||||
}
|
||||
)
|
||||
keys_handled.add(k)
|
||||
continue
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
legacy_col = ACTIVITY_LOG_LEGACY_COLUMN_FOR_EAV_PRIMARY_PARAM.get(k)
|
||||
if legacy_col and legacy_col in header and header[legacy_col] is not None:
|
||||
|
|
@ -379,9 +398,14 @@ def merge_column_backed_and_eav_metrics(
|
|||
}
|
||||
)
|
||||
keys_handled.add(k)
|
||||
continue
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
if k in eav_by_key:
|
||||
merged.append(dict(eav_by_key[k]))
|
||||
keys_handled.add(k)
|
||||
|
||||
merged.sort(key=lambda x: x["key"])
|
||||
return merged
|
||||
|
||||
|
|
|
|||
|
|
@ -307,6 +307,58 @@ def test_merge_eav_primary_falls_back_to_legacy_hr_min_column():
|
|||
assert out[0]["value"] == 88
|
||||
|
||||
|
||||
def test_merge_eav_primary_prefers_legacy_column_over_eav_when_both():
|
||||
"""Kanon: bei min_hr + hr_min und EAV-Zeile gewinnt activity_log (hr_min)."""
|
||||
schema = [
|
||||
{
|
||||
"training_parameter_id": 9,
|
||||
"key": "min_hr",
|
||||
"data_type": "integer",
|
||||
"unit": "bpm",
|
||||
"validation_rules": {},
|
||||
"source_field": None,
|
||||
}
|
||||
]
|
||||
eav = [
|
||||
{
|
||||
"training_parameter_id": 9,
|
||||
"key": "min_hr",
|
||||
"data_type": "integer",
|
||||
"unit": "bpm",
|
||||
"value": 100,
|
||||
}
|
||||
]
|
||||
out = merge_column_backed_and_eav_metrics({"hr_min": 88}, schema, eav)
|
||||
assert len(out) == 1
|
||||
assert out[0]["value"] == 88
|
||||
|
||||
|
||||
def test_merge_registry_key_prefers_activity_log_column_over_eav():
|
||||
"""Parameter-Key = Registry-Feld (z. B. duration_min): Spalte vor EAV."""
|
||||
schema = [
|
||||
{
|
||||
"training_parameter_id": 3,
|
||||
"key": "duration_min",
|
||||
"data_type": "float",
|
||||
"unit": "min",
|
||||
"validation_rules": {},
|
||||
"source_field": None,
|
||||
}
|
||||
]
|
||||
eav = [
|
||||
{
|
||||
"training_parameter_id": 3,
|
||||
"key": "duration_min",
|
||||
"data_type": "float",
|
||||
"unit": "min",
|
||||
"value": 99.0,
|
||||
}
|
||||
]
|
||||
out = merge_column_backed_and_eav_metrics({"duration_min": 45.0}, schema, eav)
|
||||
assert len(out) == 1
|
||||
assert out[0]["value"] == 45.0
|
||||
|
||||
|
||||
@patch("data_layer.activity_session_metrics.resolve_activity_attribute_schema")
|
||||
def test_upsert_csv_skips_eav_when_source_field_maps_activity_log(mock_schema):
|
||||
"""Parameter mit source_field: kanonisch activity_log — kein doppeltes EAV (z. B. avg_hr → hr_avg)."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user