-- Migration 008: M:N Exercise Relations + Hierarchical Catalogs -- Erstellt: 2026-04-23 -- Beschreibung: Umstellung von 1:1 auf M:N Beziehungen + Hierarchie Fokusbereich → Stil → Zielgruppe -- ============================================================================ -- PHASE 1: Hierarchische Struktur aufbauen -- ============================================================================ -- training_styles erweitern: Hierarchie zu focus_areas DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='training_styles' AND column_name='focus_area_id') THEN ALTER TABLE training_styles ADD COLUMN focus_area_id INT REFERENCES focus_areas(id); END IF; END $$; CREATE INDEX IF NOT EXISTS idx_training_styles_focus_area ON training_styles(focus_area_id); -- Zielgruppen-Tabelle (NEU) CREATE TABLE IF NOT EXISTS target_groups ( id SERIAL PRIMARY KEY, training_style_id INT REFERENCES training_styles(id), -- Hierarchie-Link name VARCHAR(100) NOT NULL, description TEXT, min_age INT, max_age INT, sort_order INT, status VARCHAR(50) DEFAULT 'active', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_target_groups_style ON target_groups(training_style_id); CREATE INDEX IF NOT EXISTS idx_target_groups_status ON target_groups(status); -- ============================================================================ -- PHASE 2: M:N Zuordnungstabellen -- ============================================================================ -- Übung ↔ Fokusbereiche (M:N) CREATE TABLE IF NOT EXISTS exercise_focus_areas ( id SERIAL PRIMARY KEY, exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE, focus_area_id INT REFERENCES focus_areas(id), is_primary BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT NOW(), UNIQUE(exercise_id, focus_area_id) ); CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_exercise ON exercise_focus_areas(exercise_id); CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_focus ON exercise_focus_areas(focus_area_id); CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_primary ON exercise_focus_areas(is_primary); -- Übung ↔ Stile (M:N) CREATE TABLE IF NOT EXISTS exercise_styles ( id SERIAL PRIMARY KEY, exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE, training_style_id INT REFERENCES training_styles(id), is_primary BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT NOW(), UNIQUE(exercise_id, training_style_id) ); CREATE INDEX IF NOT EXISTS idx_exercise_styles_exercise ON exercise_styles(exercise_id); CREATE INDEX IF NOT EXISTS idx_exercise_styles_style ON exercise_styles(training_style_id); CREATE INDEX IF NOT EXISTS idx_exercise_styles_primary ON exercise_styles(is_primary); -- Übung ↔ Zielgruppen (M:N) CREATE TABLE IF NOT EXISTS exercise_target_groups ( id SERIAL PRIMARY KEY, exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE, target_group_id INT REFERENCES target_groups(id), is_primary BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT NOW(), UNIQUE(exercise_id, target_group_id) ); CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_exercise ON exercise_target_groups(exercise_id); CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_target ON exercise_target_groups(target_group_id); CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_primary ON exercise_target_groups(is_primary); -- Übung ↔ Altersgruppen (M:N - separate Dimension) CREATE TABLE IF NOT EXISTS exercise_age_groups ( id SERIAL PRIMARY KEY, exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE, age_group VARCHAR(50) NOT NULL, -- 'Minis', 'Kinder', 'Schüler', 'Teenager', 'Erwachsene' created_at TIMESTAMP DEFAULT NOW(), UNIQUE(exercise_id, age_group), CHECK (age_group IN ('Minis', 'Kinder', 'Schüler', 'Teenager', 'Erwachsene')) ); CREATE INDEX IF NOT EXISTS idx_exercise_age_groups_exercise ON exercise_age_groups(exercise_id); CREATE INDEX IF NOT EXISTS idx_exercise_age_groups_age ON exercise_age_groups(age_group); -- ============================================================================ -- PHASE 3: Daten-Migration (Altdaten von 1:1 zu M:N) -- ============================================================================ -- Migriere focus_area_id → exercise_focus_areas (als primäre Zuordnung) INSERT INTO exercise_focus_areas (exercise_id, focus_area_id, is_primary) SELECT id, focus_area_id, true FROM exercises WHERE focus_area_id IS NOT NULL ON CONFLICT (exercise_id, focus_area_id) DO NOTHING; -- Migriere training_style_id → exercise_styles (als primäre Zuordnung) INSERT INTO exercise_styles (exercise_id, training_style_id, is_primary) SELECT id, training_style_id, true FROM exercises WHERE training_style_id IS NOT NULL ON CONFLICT (exercise_id, training_style_id) DO NOTHING; -- ============================================================================ -- PHASE 4: Basis-Daten für Zielgruppen -- ============================================================================ -- Beispiel-Zielgruppen für Shotokan (training_style_id = 1, falls vorhanden) DO $$ DECLARE shotokan_id INT; BEGIN SELECT id INTO shotokan_id FROM training_styles WHERE name = 'Shotokan' LIMIT 1; IF shotokan_id IS NOT NULL THEN INSERT INTO target_groups (training_style_id, name, description, sort_order) VALUES (shotokan_id, 'Breitensportler', 'Karate für Freizeit und Fitness', 1), (shotokan_id, 'Leistungssportler', 'Wettkampforientiertes Training', 2), (shotokan_id, 'Kinder', 'Karate für Kinder (6-12 Jahre)', 3) ON CONFLICT DO NOTHING; END IF; END $$; -- ============================================================================ -- PHASE 5: Hierarchie-Daten (Stile zu Fokusbereichen zuordnen) -- ============================================================================ -- Ordne Karate-Stile dem Fokusbereich "Karate" zu DO $$ DECLARE karate_id INT; BEGIN SELECT id INTO karate_id FROM focus_areas WHERE name = 'Karate' LIMIT 1; IF karate_id IS NOT NULL THEN UPDATE training_styles SET focus_area_id = karate_id WHERE name IN ('Shotokan', 'Goju-Ryu', 'Wado-Ryu', 'Shito-Ryu', 'Kyokushin') AND focus_area_id IS NULL; END IF; END $$; -- ============================================================================ -- HINWEISE für spätere Migrationen -- ============================================================================ -- Die folgenden Spalten in exercises können später gedroppt werden: -- - focus_area_id (deprecated, durch exercise_focus_areas ersetzt) -- - training_style_id (deprecated, durch exercise_styles ersetzt) -- - training_character_id (bleibt vorerst, separate Dimension) -- Aktuell bleiben sie zur Rückwärtskompatibilität erhalten. -- Neue Übungen sollten NUR über M:N-Tabellen zugeordnet werden. -- API-Endpoints sollten enriched data aus M:N-Tabellen liefern.