2026-04-09 21:37:19 +02:00
|
|
|
|
-- 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",
|
2026-04-10 10:42:59 +02:00
|
|
|
|
"source_unit": "kj",
|
2026-04-09 21:37:19 +02:00
|
|
|
|
"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"},
|
|
|
|
|
|
"end_time": {"type": "datetime", "format": "yyyy-mm-dd 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 = '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 (kcal)', 'Dauer', 'Durchschnittliche Herzfrequenz (bpm)', 'Ende', '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",
|
|
|
|
|
|
"Durchschnittliche Herzfrequenz (bpm)": "hr_avg"
|
|
|
|
|
|
}'::JSONB,
|
|
|
|
|
|
'{
|
|
|
|
|
|
"start_time": {"type": "datetime", "format": "yyyy-mm-dd HH:MM:SS", "extract": "date_and_time"},
|
|
|
|
|
|
"end_time": {"type": "datetime", "format": "yyyy-mm-dd 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 = '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'
|
|
|
|
|
|
);
|