mitai-jinkendo/backend/migrations/043_csv_parser_seed_templates.sql
Lars 6945b748cb
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Build Test / pytest-backend (push) Successful in 3s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s
feat(schema, csv_parser): Update activity log schema and parsing logic
- Increased precision for `kcal_active`, `kcal_resting`, `hr_avg`, and `hr_max` fields in the activity log schema.
- Added a new function `_activity_hr_bpm` to validate heart rate values during CSV import, ensuring they fall within plausible ranges.
- Updated the CSV parser to utilize the new heart rate validation function for improved data integrity.
- Enhanced the type converter to accommodate additional aliases for energy fields in CSV imports.
- Added a test to verify conversion of active energy from kJ to kcal, ensuring accurate data handling.
2026-04-11 06:41:23 +02:00

316 lines
12 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Migration 043: CSV Parser System-Templates (Issue #21)
-- Idempotent: pro Template nur einfügen, wenn noch kein System-Eintrag für module+mapping_name existiert.
INSERT INTO csv_field_mappings (
profile_id, is_system, module, mapping_name, description,
column_signature, delimiter, encoding, has_header,
field_mappings, type_conversions
)
SELECT
NULL,
true,
'nutrition',
'FDDB Export (Standard)',
'Standard-Format für FDDB.de CSV-Exporte (Deutsch). Delimiter Semikolon, kJ → kcal Konvertierung.',
ARRAY['datum_tag_monat_jahr_stunde_minute', 'fett_g', 'kh_g', 'kj', 'protein_g']::TEXT[],
';',
'utf-8',
true,
'{
"datum_tag_monat_jahr_stunde_minute": "date",
"kj": "kcal",
"fett_g": "fat_g",
"kh_g": "carbs_g",
"protein_g": "protein_g"
}'::JSONB,
'{
"date": {
"type": "date",
"format": "dd.mm.yyyy HH:MM",
"extract": "date_only"
},
"kcal": {
"type": "float",
"source_unit": "kj",
"decimal_separator": ","
},
"fat_g": {"type": "float", "decimal_separator": ","},
"carbs_g": {"type": "float", "decimal_separator": ","},
"protein_g": {"type": "float", "decimal_separator": ","}
}'::JSONB
WHERE NOT EXISTS (
SELECT 1 FROM csv_field_mappings f
WHERE f.is_system AND f.profile_id IS NULL
AND f.module = 'nutrition' AND f.mapping_name = 'FDDB Export (Standard)'
);
INSERT INTO csv_field_mappings (
profile_id, is_system, module, mapping_name, description,
column_signature, delimiter, encoding, has_header,
field_mappings, type_conversions
)
SELECT
NULL, true, 'nutrition', 'MyFitnessPal Export',
'Standard CSV export from MyFitnessPal (English)',
ARRAY['Carbohydrates (g)', 'Calories', 'Date', 'Fat (g)', 'Protein (g)']::TEXT[],
',', 'utf-8', true,
'{
"Date": "date",
"Calories": "kcal",
"Fat (g)": "fat_g",
"Carbohydrates (g)": "carbs_g",
"Protein (g)": "protein_g"
}'::JSONB,
'{
"date": {"type": "date", "format": "yyyy-mm-dd"},
"kcal": {"type": "float", "decimal_separator": "."},
"fat_g": {"type": "float", "decimal_separator": "."},
"carbs_g": {"type": "float", "decimal_separator": "."},
"protein_g": {"type": "float", "decimal_separator": "."}
}'::JSONB
WHERE NOT EXISTS (
SELECT 1 FROM csv_field_mappings f
WHERE f.is_system AND f.profile_id IS NULL
AND f.module = 'nutrition' AND f.mapping_name = 'MyFitnessPal Export'
);
INSERT INTO csv_field_mappings (
profile_id, is_system, module, mapping_name, description,
column_signature, delimiter, encoding, has_header,
field_mappings, type_conversions
)
SELECT
NULL, true, 'nutrition', 'Cronometer Export',
'Cronometer daily nutrition export (English)',
ARRAY['Day', 'Energy (kcal)', 'Fat (g)', 'Net Carbs (g)', 'Protein (g)']::TEXT[],
',', 'utf-8', true,
'{
"Day": "date",
"Energy (kcal)": "kcal",
"Fat (g)": "fat_g",
"Net Carbs (g)": "carbs_g",
"Protein (g)": "protein_g"
}'::JSONB,
'{
"date": {"type": "date", "format": "yyyy-mm-dd"},
"kcal": {"type": "float", "decimal_separator": "."},
"fat_g": {"type": "float", "decimal_separator": "."},
"carbs_g": {"type": "float", "decimal_separator": "."},
"protein_g": {"type": "float", "decimal_separator": "."}
}'::JSONB
WHERE NOT EXISTS (
SELECT 1 FROM csv_field_mappings f
WHERE f.is_system AND f.profile_id IS NULL
AND f.module = 'nutrition' AND f.mapping_name = 'Cronometer Export'
);
INSERT INTO csv_field_mappings (
profile_id, is_system, module, mapping_name, description,
column_signature, delimiter, encoding, has_header,
field_mappings, type_conversions
)
SELECT
NULL, true, 'activity', 'Apple Health Workout Export (English)',
'Apple Health CSV-Export für Workouts (English). Automatisches Training-Type-Mapping.',
ARRAY['Active Energy (kcal)', 'Distance (km)', 'Duration', 'End', 'Heart Rate Average (bpm)', 'Start', 'Workout Type']::TEXT[],
',', 'utf-8', true,
'{
"Workout Type": "activity_type",
"Start": "start_time",
"End": "end_time",
"Duration": "duration_min",
"Distance (km)": "distance_km",
"Active Energy (kcal)": "kcal_active",
"Heart Rate Average (bpm)": "hr_avg"
}'::JSONB,
'{
"start_time": {"type": "datetime", "format": "yyyy-mm-dd HH:MM:SS", "extract": "date_and_time", "flexible": true},
"end_time": {"type": "datetime", "format": "yyyy-mm-dd HH:MM:SS", "flexible": true},
"duration_min": {"type": "duration", "format": "HH:MM:SS", "target_unit": "minutes"},
"distance_km": {"type": "float", "decimal_separator": "."},
"kcal_active": {"type": "float", "decimal_separator": "."},
"hr_avg": {"type": "int"}
}'::JSONB
WHERE NOT EXISTS (
SELECT 1 FROM csv_field_mappings f
WHERE f.is_system AND f.profile_id IS NULL
AND f.module = 'activity' AND f.mapping_name = 'Apple Health Workout Export (English)'
);
INSERT INTO csv_field_mappings (
profile_id, is_system, module, mapping_name, description,
column_signature, delimiter, encoding, has_header,
field_mappings, type_conversions
)
SELECT
NULL, true, 'activity', 'Apple Health Workout Export (Deutsch)',
'Apple Health CSV-Export für Workouts (Deutsch). Automatisches Training-Type-Mapping.',
ARRAY['Aktive Energie (kJ)', 'Aktive Energie (kcal)', 'Dauer', 'Durchschnittliche Herzfrequenz (bpm)', 'Ende', 'Ruheeinträge (kJ)', 'Start', 'Strecke (km)', 'Trainingsart']::TEXT[],
',', 'utf-8', true,
'{
"Trainingsart": "activity_type",
"Start": "start_time",
"Ende": "end_time",
"Dauer": "duration_min",
"Strecke (km)": "distance_km",
"Aktive Energie (kcal)": "kcal_active",
"Aktive Energie (kJ)": "kcal_active",
"Ruheeinträge (kJ)": "kcal_resting",
"Durchschnittliche Herzfrequenz (bpm)": "hr_avg"
}'::JSONB,
'{
"start_time": {"type": "datetime", "format": "yyyy-mm-dd HH:MM:SS", "extract": "date_and_time", "flexible": true},
"end_time": {"type": "datetime", "format": "yyyy-mm-dd HH:MM:SS", "flexible": true},
"duration_min": {"type": "duration", "format": "HH:MM:SS", "target_unit": "minutes"},
"distance_km": {"type": "float", "decimal_separator": ",", "flexible": true},
"kcal_active": {"type": "float", "decimal_separator": ".", "flexible": true, "source_unit": "kj"},
"kcal_resting": {"type": "float", "decimal_separator": ".", "flexible": true, "source_unit": "kj"},
"hr_avg": {"type": "int", "flexible": true}
}'::JSONB
WHERE NOT EXISTS (
SELECT 1 FROM csv_field_mappings f
WHERE f.is_system AND f.profile_id IS NULL
AND f.module = 'activity' AND f.mapping_name = 'Apple Health Workout Export (Deutsch)'
);
INSERT INTO csv_field_mappings (
profile_id, is_system, module, mapping_name, description,
column_signature, delimiter, encoding, has_header,
field_mappings, type_conversions
)
SELECT
NULL, true, 'activity', 'Garmin Connect Export',
'Garmin Connect activity CSV export (English)',
ARRAY['Activity Type', 'Avg HR', 'Calories', 'Date', 'Distance', 'Duration', 'Time']::TEXT[],
',', 'utf-8', true,
'{
"Activity Type": "activity_type",
"Date": "date",
"Time": "start_time",
"Duration": "duration_min",
"Distance": "distance_km",
"Calories": "kcal_active",
"Avg HR": "hr_avg"
}'::JSONB,
'{
"date": {"type": "date", "format": "yyyy-mm-dd"},
"start_time": {"type": "time", "format": "HH:MM:SS"},
"duration_min": {"type": "duration", "format": "HH:MM:SS", "target_unit": "minutes"},
"distance_km": {"type": "float", "decimal_separator": "."},
"kcal_active": {"type": "float", "decimal_separator": "."},
"hr_avg": {"type": "int"}
}'::JSONB
WHERE NOT EXISTS (
SELECT 1 FROM csv_field_mappings f
WHERE f.is_system AND f.profile_id IS NULL
AND f.module = 'activity' AND f.mapping_name = 'Garmin Connect Export'
);
INSERT INTO csv_field_mappings (
profile_id, is_system, module, mapping_name, description,
column_signature, delimiter, encoding, has_header,
field_mappings, type_conversions
)
SELECT
NULL, true, 'blood_pressure', 'Omron Export (Deutsch)',
'Omron Blutdruckmessgerät CSV-Export (Deutsch)',
ARRAY['Datum', 'Diastolisch (mmHg)', 'Puls (bpm)', 'Systolisch (mmHg)', 'Zeit']::TEXT[],
',', 'utf-8', true,
'{
"Datum": "measured_date",
"Zeit": "measured_time",
"Systolisch (mmHg)": "systolic",
"Diastolisch (mmHg)": "diastolic",
"Puls (bpm)": "pulse"
}'::JSONB,
'{
"measured_date": {"type": "date", "format": "dd.mm.yyyy"},
"measured_time": {"type": "time", "format": "HH:MM"},
"systolic": {"type": "int"},
"diastolic": {"type": "int"},
"pulse": {"type": "int"}
}'::JSONB
WHERE NOT EXISTS (
SELECT 1 FROM csv_field_mappings f
WHERE f.is_system AND f.profile_id IS NULL
AND f.module = 'blood_pressure' AND f.mapping_name = 'Omron Export (Deutsch)'
);
INSERT INTO csv_field_mappings (
profile_id, is_system, module, mapping_name, description,
column_signature, delimiter, encoding, has_header,
field_mappings, type_conversions
)
SELECT
NULL, true, 'blood_pressure', 'Omron Export (English)',
'Omron blood pressure monitor CSV export (English)',
ARRAY['Date', 'Diastolic (mmHg)', 'Pulse (bpm)', 'Systolic (mmHg)', 'Time']::TEXT[],
',', 'utf-8', true,
'{
"Date": "measured_date",
"Time": "measured_time",
"Systolic (mmHg)": "systolic",
"Diastolic (mmHg)": "diastolic",
"Pulse (bpm)": "pulse"
}'::JSONB,
'{
"measured_date": {"type": "date", "format": "mm/dd/yyyy"},
"measured_time": {"type": "time", "format": "HH:MM"},
"systolic": {"type": "int"},
"diastolic": {"type": "int"},
"pulse": {"type": "int"}
}'::JSONB
WHERE NOT EXISTS (
SELECT 1 FROM csv_field_mappings f
WHERE f.is_system AND f.profile_id IS NULL
AND f.module = 'blood_pressure' AND f.mapping_name = 'Omron Export (English)'
);
INSERT INTO csv_field_mappings (
profile_id, is_system, module, mapping_name, description,
column_signature, delimiter, encoding, has_header,
field_mappings, type_conversions
)
SELECT
NULL, true, 'weight', 'Apple Health Weight Export',
'Apple Health body mass CSV export',
ARRAY['Body Mass (kg)', 'Start']::TEXT[],
',', 'utf-8', true,
'{
"Start": "date",
"Body Mass (kg)": "weight"
}'::JSONB,
'{
"date": {"type": "datetime", "format": "yyyy-mm-dd HH:MM:SS", "extract": "date_only"},
"weight": {"type": "float", "decimal_separator": "."}
}'::JSONB
WHERE NOT EXISTS (
SELECT 1 FROM csv_field_mappings f
WHERE f.is_system AND f.profile_id IS NULL
AND f.module = 'weight' AND f.mapping_name = 'Apple Health Weight Export'
);
INSERT INTO csv_field_mappings (
profile_id, is_system, module, mapping_name, description,
column_signature, delimiter, encoding, has_header,
field_mappings, type_conversions
)
SELECT
NULL, true, 'weight', 'Withings Export',
'Withings smart scale CSV export (weight, body fat, muscle mass)',
ARRAY['Body Fat (%)', 'Date', 'Muscle Mass (kg)', 'Weight (kg)']::TEXT[],
',', 'utf-8', true,
'{
"Date": "date",
"Weight (kg)": "weight"
}'::JSONB,
'{
"date": {"type": "date", "format": "yyyy-mm-dd"},
"weight": {"type": "float", "decimal_separator": "."}
}'::JSONB
WHERE NOT EXISTS (
SELECT 1 FROM csv_field_mappings f
WHERE f.is_system AND f.profile_id IS NULL
AND f.module = 'weight' AND f.mapping_name = 'Withings Export'
);