shinkan-jinkendo/.claude/docs/technical/EXERCISES_DATABASE_FINAL.md
Lars f5895b6637
Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 41s
chore: update documentation and enhance exercise progression graph details
- Updated CLAUDE.md to reflect the addition of exercise_progression_graphs in the backend routers.
- Revised PROJECT_STATUS.md to document the current project status and recent milestones, including the implementation of the exercise progression graph feature.
- Incremented versioning in DOMAIN_MODEL.md and DATABASE_SCHEMA.md to align with the latest migration updates.
- Enhanced technical specifications in TRAINING_FRAMEWORK_SPEC.md to clarify the implementation details of the exercise progression graph and its integration with the training framework.
2026-05-05 08:30:48 +02:00

913 lines
30 KiB
Markdown
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.

# Database Schema Final - Exercises System
**Version:** 1.2
**Datum:** 2026-04-24
**Status:** REVIEWED - Pending Implementation
**Autor:** Claude Code
**Änderungen v1.2:** Skill-Level auf benannte Stufen (einsteigerexperte), intensity auf niedrig/mittel/hoch, ai_suggested Felder, summary_ai_generated
**Änderungen v1.1:** age_groups JSONB entfernt, Legacy-DROP ergänzt, Migration 017 (Exercise Blocks)
---
## 1. Übersicht
**Neue Migrationen:**
- **Migration 014:** Variant Progression System + Search Vector + Legacy-Cleanup
- **Migration 015:** Semantic Matching (OPTIONAL - Phase 2)
- **Migration 016:** Saved Searches
- **Migration 017:** Exercise Blocks + Template Blocks
**Basis:** Migrationen 001-013 (bereits deployed)
**Progressionsgraph zwischen Übungen:** Migrationen **032034** — nicht Bestandteil dieses „Exercise Catalog“-Schemas-Dokuments; siehe **`TRAINING_FRAMEWORK_SPEC.md`** §3 und **`DATABASE_SCHEMA.md`** (Migrationshistorie).
---
## 2. Migration 014: Variant Progression + Search
### 2.1 Vollständige Migration
```sql
-- Migration 014: Variant Progression System + Search Vector
-- Autor: Claude Code
-- Datum: 2026-04-24
DO $$
BEGIN
-- ============================================================================
-- VARIANT PROGRESSION
-- ============================================================================
-- Erweitere exercise_variants Tabelle
ALTER TABLE exercise_variants
ADD COLUMN IF NOT EXISTS progression_level INT DEFAULT 1 CHECK (progression_level BETWEEN 1 AND 10),
ADD COLUMN IF NOT EXISTS sequence_order INT,
ADD COLUMN IF NOT EXISTS prerequisite_variant_id INT REFERENCES exercise_variants(id) ON DELETE SET NULL;
-- Index für Prerequisites
CREATE INDEX IF NOT EXISTS idx_exercise_variants_prerequisite
ON exercise_variants(prerequisite_variant_id);
-- ============================================================================
-- SEARCH VECTOR (Volltext-Suche)
-- ============================================================================
-- Füge search_vector zu exercises hinzu
ALTER TABLE exercises
ADD COLUMN IF NOT EXISTS search_vector tsvector;
-- Index für Volltext-Suche
CREATE INDEX IF NOT EXISTS idx_exercises_search
ON exercises USING gin(search_vector);
-- Funktion für automatisches Update
CREATE OR REPLACE FUNCTION update_exercises_search_vector()
RETURNS trigger AS $func$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('german', COALESCE(NEW.title, '')), 'A') ||
setweight(to_tsvector('german', COALESCE(NEW.summary, '')), 'B') ||
setweight(to_tsvector('german', COALESCE(NEW.execution, '')), 'C') ||
setweight(to_tsvector('german', COALESCE(NEW.trainer_notes, '')), 'D');
RETURN NEW;
END;
$func$ LANGUAGE plpgsql;
-- Trigger
DROP TRIGGER IF EXISTS exercises_search_update ON exercises;
CREATE TRIGGER exercises_search_update
BEFORE INSERT OR UPDATE ON exercises
FOR EACH ROW EXECUTE FUNCTION update_exercises_search_vector();
-- Initiales Befüllen (für existierende Zeilen)
UPDATE exercises SET search_vector = (
setweight(to_tsvector('german', COALESCE(title, '')), 'A') ||
setweight(to_tsvector('german', COALESCE(summary, '')), 'B') ||
setweight(to_tsvector('german', COALESCE(execution, '')), 'C') ||
setweight(to_tsvector('german', COALESCE(trainer_notes, '')), 'D')
) WHERE search_vector IS NULL;
-- ============================================================================
-- LEGACY COLUMN CLEANUP
-- Deprecated Felder aus exercises (ersetzt durch M:N Tabellen in Migration 008+)
-- ============================================================================
-- age_groups JSONB → ersetzt durch exercise_age_groups M:N (seit Migration 008)
ALTER TABLE exercises DROP COLUMN IF EXISTS age_groups;
-- focus_area VARCHAR → ersetzt durch exercise_focus_areas M:N (seit Migration 008)
ALTER TABLE exercises DROP COLUMN IF EXISTS focus_area;
-- secondary_areas JSONB → ersetzt durch exercise_focus_areas M:N
ALTER TABLE exercises DROP COLUMN IF EXISTS secondary_areas;
-- training_character VARCHAR → ersetzt durch exercise_training_characters M:N (seit Migration 012)
ALTER TABLE exercises DROP COLUMN IF EXISTS training_character;
-- ============================================================================
-- ADDITIONAL INDEXES (Performance)
-- ============================================================================
-- Häufige Filter
CREATE INDEX IF NOT EXISTS idx_exercises_visibility ON exercises(visibility);
CREATE INDEX IF NOT EXISTS idx_exercises_status ON exercises(status);
CREATE INDEX IF NOT EXISTS idx_exercises_created_at ON exercises(created_at DESC);
-- M:N Relations (falls noch nicht vorhanden)
CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_focus
ON exercise_focus_areas(focus_area_id);
CREATE INDEX IF NOT EXISTS idx_exercise_styles_style
ON exercise_training_styles(training_style_id);
CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_group
ON exercise_target_groups(target_group_id);
CREATE INDEX IF NOT EXISTS idx_exercise_skills_skill
ON exercise_skills(skill_id);
RAISE NOTICE 'Migration 014 completed successfully';
END $$;
```
---
## 3. Migration 015: Semantic Matching (Optional Phase 2)
**Diese Migration ist optional für MVP Phase 1**
```sql
-- Migration 015: Semantic Exercise Matching (OPTIONAL - Phase 2)
-- Nutzt pgvector extension für Similarity-Matching
-- Autor: Claude Code
-- Datum: 2026-04-24
DO $$
BEGIN
-- ============================================================================
-- SEMANTIC EMBEDDINGS (Optional)
-- ============================================================================
-- Benötigt pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Erweitere exercises Tabelle
ALTER TABLE exercises
ADD COLUMN IF NOT EXISTS embedding vector(1536); -- OpenAI ada-002 embedding size
-- Index für Ähnlichkeitssuche
CREATE INDEX IF NOT EXISTS idx_exercises_embedding
ON exercises USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Funktion für Similarity-Search
CREATE OR REPLACE FUNCTION find_similar_exercises(
query_embedding vector(1536),
limit_count INT DEFAULT 10
)
RETURNS TABLE (
exercise_id INT,
title VARCHAR,
similarity FLOAT
) AS $func$
BEGIN
RETURN QUERY
SELECT
id,
title,
1 - (embedding <=> query_embedding) AS similarity
FROM exercises
WHERE embedding IS NOT NULL
ORDER BY embedding <=> query_embedding
LIMIT limit_count;
END;
$func$ LANGUAGE plpgsql;
RAISE NOTICE 'Migration 015 completed successfully (Semantic Matching - OPTIONAL)';
END $$;
```
**Hinweis:** Embeddings werden über separaten Background-Job befüllt, nicht beim INSERT.
---
## 4. Migration 016: Saved Searches
```sql
-- Migration 016: Saved Exercise Searches
-- Autor: Claude Code
-- Datum: 2026-04-24
DO $$
BEGIN
-- ============================================================================
-- SAVED SEARCHES
-- ============================================================================
CREATE TABLE IF NOT EXISTS saved_exercise_searches (
id SERIAL PRIMARY KEY,
profile_id INT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
filters JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Index für User-Zugriff
CREATE INDEX IF NOT EXISTS idx_saved_searches_profile
ON saved_exercise_searches(profile_id);
-- Trigger für updated_at
CREATE OR REPLACE FUNCTION update_saved_searches_timestamp()
RETURNS trigger AS $func$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$func$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS saved_searches_update ON saved_exercise_searches;
CREATE TRIGGER saved_searches_update
BEFORE UPDATE ON saved_exercise_searches
FOR EACH ROW EXECUTE FUNCTION update_saved_searches_timestamp();
RAISE NOTICE 'Migration 016 completed successfully';
END $$;
```
---
## 5. Migration 017: Exercise Blocks + Template Blocks
```sql
-- Migration 017: Exercise Blocks + Template Blocks
-- Autor: Claude Code
-- Datum: 2026-04-24
-- Zweck: Gruppierung verschiedener Übungen in Blöcken (User-Anforderung #4)
-- Series = Varianten-Progression (via exercise_variants, KEINE eigene Tabelle)
-- Blocks = Verschiedene Übungen in Reihenfolge (DIESE Migration)
DO $$
BEGIN
-- ============================================================================
-- EXERCISE BLOCKS
-- Eine Sammlung verschiedener Übungen in definierter Reihenfolge
-- ============================================================================
CREATE TABLE IF NOT EXISTS exercise_blocks (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
goal TEXT,
-- Template-Modus: Block mit Platzhaltern für flexible Planung
is_template BOOLEAN DEFAULT false,
-- Ownership & Sichtbarkeit
club_id INT REFERENCES clubs(id) ON DELETE SET NULL,
created_by INT REFERENCES profiles(id) ON DELETE SET NULL,
visibility VARCHAR(20) DEFAULT 'private' CHECK (visibility IN ('private', 'club', 'official')),
-- Timestamps
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- ============================================================================
-- EXERCISE BLOCK ITEMS
-- Einzelne Positionen innerhalb eines Blocks
-- ============================================================================
CREATE TABLE IF NOT EXISTS exercise_block_items (
id SERIAL PRIMARY KEY,
block_id INT NOT NULL REFERENCES exercise_blocks(id) ON DELETE CASCADE,
-- Konkrete Übung (NULL wenn is_placeholder = true)
exercise_id INT REFERENCES exercises(id) ON DELETE RESTRICT,
-- Optionale Variante (kann NULL sein → Haupt-Übung wird genutzt)
variant_id INT REFERENCES exercise_variants(id) ON DELETE SET NULL,
-- Reihenfolge innerhalb des Blocks
sequence_order INT NOT NULL,
-- Template-Modus: Platzhalter statt konkreter Übung
is_placeholder BOOLEAN DEFAULT false,
-- Kriterien für Platzhalter-Auflösung (nur relevant wenn is_placeholder = true)
-- Schema: {"focus_area_id": 1, "max_duration": 10, "skill_ids": [3, 7], "difficulty": "easier"}
-- Alle Felder optional, werden als AND-Filter bei der Übungssuche genutzt
placeholder_criteria JSONB,
-- Platzhalter-Beschriftung (für UX im Template-Modus)
placeholder_label VARCHAR(100), -- z.B. "Aufwärmübung Schlag", "Hauptübung Kumite"
-- Zusätzliche Notizen für diese Position
notes TEXT,
-- Timestamps
created_at TIMESTAMP DEFAULT NOW(),
-- Constraints
UNIQUE(block_id, sequence_order),
-- Entweder exercise_id ODER is_placeholder=true
CHECK (
(is_placeholder = false AND exercise_id IS NOT NULL) OR
(is_placeholder = true AND exercise_id IS NULL)
)
);
-- ============================================================================
-- INDEXES
-- ============================================================================
CREATE INDEX IF NOT EXISTS idx_exercise_blocks_club ON exercise_blocks(club_id);
CREATE INDEX IF NOT EXISTS idx_exercise_blocks_creator ON exercise_blocks(created_by);
CREATE INDEX IF NOT EXISTS idx_exercise_blocks_visibility ON exercise_blocks(visibility);
CREATE INDEX IF NOT EXISTS idx_exercise_blocks_template ON exercise_blocks(is_template) WHERE is_template = true;
CREATE INDEX IF NOT EXISTS idx_exercise_block_items_block ON exercise_block_items(block_id);
CREATE INDEX IF NOT EXISTS idx_exercise_block_items_exercise ON exercise_block_items(exercise_id);
CREATE INDEX IF NOT EXISTS idx_exercise_block_items_placeholder ON exercise_block_items(is_placeholder) WHERE is_placeholder = true;
-- ============================================================================
-- UPDATED_AT TRIGGER
-- ============================================================================
DROP TRIGGER IF EXISTS exercise_blocks_update ON exercise_blocks;
CREATE TRIGGER exercise_blocks_update
BEFORE UPDATE ON exercise_blocks
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
RAISE NOTICE 'Migration 017 completed successfully (Exercise Blocks)';
END $$;
```
### 5.0 Placeholder Criteria Schema
Das `placeholder_criteria` JSONB-Feld in `exercise_block_items` erlaubt folgende Schlüssel (alle optional, werden als AND-Filter kombiniert):
```json
{
"focus_area_id": 1, // INT: Fokusbereich-ID
"training_style_id": 2, // INT: Stil-ID
"target_group_id": 5, // INT: Zielgruppen-ID
"skill_ids": [3, 7], // INT[]: Mindestens eine dieser Fähigkeiten
"max_duration": 15, // INT: Maximale Dauer in Minuten
"min_duration": 5, // INT: Minimale Dauer in Minuten
"difficulty": "easier", // "easier" | "same" | "harder"
"visibility": "club" // "private" | "club" | "official"
}
```
**Validierung:** Backend prüft, dass alle vorhandenen Schlüssel bekannte Felder sind und die Werte dem erwarteten Typ entsprechen.
---
## 6. Vollständige Tabellenstruktur (Final State)
### 5.1 exercises (Kern-Tabelle)
```sql
CREATE TABLE exercises (
id SERIAL PRIMARY KEY,
-- Basis-Info
title VARCHAR(300) NOT NULL,
summary TEXT,
goal TEXT NOT NULL,
execution TEXT NOT NULL,
preparation TEXT,
trainer_notes TEXT,
-- Dauer & Gruppengröße
duration_min INT,
duration_max INT,
group_size_min INT,
group_size_max INT,
-- JSONB-Felder
equipment JSONB DEFAULT '[]'::jsonb,
-- HINWEIS: age_groups werden via exercise_age_groups M:N Tabelle verwaltet (nicht JSONB)
-- Suche
search_vector tsvector, -- NEU in Migration 014
-- Semantic Matching (optional)
embedding vector(1536), -- NEU in Migration 015 (optional)
-- Sichtbarkeit & Status
visibility VARCHAR(20) DEFAULT 'private' CHECK (visibility IN ('private', 'club', 'official')),
status VARCHAR(20) DEFAULT 'draft' CHECK (status IN ('draft', 'in_review', 'approved', 'archived')),
-- Ownership
created_by INT REFERENCES profiles(id) ON DELETE SET NULL,
club_id INT REFERENCES clubs(id) ON DELETE SET NULL,
-- Import-Tracking
import_source VARCHAR(50), -- 'mediawiki', 'csv', etc.
import_id VARCHAR(100), -- Original-ID aus Quellsystem
-- Timestamps
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
### 5.2 exercise_variants
```sql
CREATE TABLE exercise_variants (
id SERIAL PRIMARY KEY,
exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
-- Variant-Details
variant_name VARCHAR(200) NOT NULL,
description TEXT,
execution_changes TEXT,
-- Dauer (Override)
duration_min INT,
duration_max INT,
-- Equipment-Änderungen
equipment_changes JSONB DEFAULT '[]'::jsonb,
-- Schwierigkeit
difficulty_adjustment VARCHAR(20) CHECK (difficulty_adjustment IN ('easier', 'same', 'harder')),
-- Progression (NEU in Migration 014)
progression_level INT DEFAULT 1 CHECK (progression_level BETWEEN 1 AND 10),
sequence_order INT,
prerequisite_variant_id INT REFERENCES exercise_variants(id) ON DELETE SET NULL,
-- Timestamps
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
### 5.3 exercise_media
```sql
CREATE TABLE exercise_media (
id SERIAL PRIMARY KEY,
exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
-- Media-Type
media_type VARCHAR(20) CHECK (media_type IN ('image', 'video', 'document', 'sketch')),
-- Lokale Datei (exklusiv mit embed_url)
file_path VARCHAR(500),
file_size INT,
mime_type VARCHAR(100),
original_filename VARCHAR(300),
-- Embed (exklusiv mit file_path)
embed_url TEXT,
embed_platform VARCHAR(50), -- 'youtube', 'vimeo', 'instagram', 'tiktok'
-- Metadata
title VARCHAR(200),
description TEXT,
sort_order INT DEFAULT 1,
is_primary BOOLEAN DEFAULT FALSE,
context VARCHAR(50) DEFAULT 'ablauf' CHECK (context IN ('ablauf', 'detail', 'trainer_hint')),
-- Timestamps
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
-- Constraint: Entweder file_path ODER embed_url
CHECK (
(file_path IS NOT NULL AND embed_url IS NULL) OR
(file_path IS NULL AND embed_url IS NOT NULL)
)
);
```
### 5.4 M:N Relation Tables
**exercise_focus_areas:**
```sql
CREATE TABLE exercise_focus_areas (
id SERIAL PRIMARY KEY,
exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
focus_area_id INT NOT NULL REFERENCES focus_areas(id) ON DELETE RESTRICT,
is_primary BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(exercise_id, focus_area_id)
);
```
**exercise_training_styles:**
```sql
CREATE TABLE exercise_training_styles (
id SERIAL PRIMARY KEY,
exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
training_style_id INT NOT NULL REFERENCES training_styles(id) ON DELETE RESTRICT,
is_primary BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(exercise_id, training_style_id)
);
```
**exercise_target_groups:**
```sql
CREATE TABLE exercise_target_groups (
id SERIAL PRIMARY KEY,
exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
target_group_id INT NOT NULL REFERENCES target_groups(id) ON DELETE RESTRICT,
is_primary BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(exercise_id, target_group_id)
);
```
**exercise_age_groups:**
```sql
CREATE TABLE exercise_age_groups (
id SERIAL PRIMARY KEY,
exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
age_group_name VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(exercise_id, age_group_name)
);
```
**exercise_skills:**
```sql
CREATE TABLE exercise_skills (
id SERIAL PRIMARY KEY,
exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
skill_id INT NOT NULL REFERENCES skills(id) ON DELETE RESTRICT,
is_primary BOOLEAN DEFAULT FALSE,
-- Kompetenzmodell: benannte Stufen (nicht numerisch 1-10)
-- NULL = nicht definiert / nicht relevant
required_level VARCHAR(20) CHECK (required_level IN ('einsteiger', 'grundlagen', 'aufbau', 'fortgeschritten', 'experte')),
target_level VARCHAR(20) CHECK (target_level IN ('einsteiger', 'grundlagen', 'aufbau', 'fortgeschritten', 'experte')),
-- Trainingsintensität: Wie stark wird diese Fähigkeit in der Übung trainiert
intensity VARCHAR(10) CHECK (intensity IN ('niedrig', 'mittel', 'hoch')),
-- KI-generierte Zuordnung (false = manuell bestätigt)
ai_suggested BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(exercise_id, skill_id)
);
```
**Skill-Level-Definitionen (Kompetenzmodell):**
| Stufe | Wert | Beschreibung | Beispiel Distanzgefühl |
|-------|------|--------------|----------------------|
| 1 | `einsteiger` | Erste Berührung mit der Fähigkeit, kein Vorwissen nötig | Versteht das Konzept Distanz |
| 2 | `grundlagen` | Grundprinzipien bekannt, in einfachen Situationen anwendbar | Hält in ruhigen Übungen Distanz |
| 3 | `aufbau` | Sicher in Standard-Situationen, braucht noch Korrektur | Distanzkontrolle in Partnerübungen |
| 4 | `fortgeschritten` | Zuverlässig auch unter Druck, wenig Fehler | Stabile Distanz im Sparring |
| 5 | `experte` | Automatisiert, intuitiv, auch in komplexen Situationen | Feines Distanzgefühl im Wettkampf |
**Für Migration 014 ergänzen:**
```sql
-- In Migration 014 nach den Variant-Columns ergänzen:
-- Skill-Level auf VARCHAR umstellen (bestehende INT-Werte migrieren)
ALTER TABLE exercise_skills
ALTER COLUMN required_level TYPE VARCHAR(20) USING
CASE required_level
WHEN 0 THEN NULL
WHEN 1 THEN 'einsteiger'
WHEN 2 THEN 'grundlagen'
WHEN 3 THEN 'aufbau'
WHEN 4 THEN 'fortgeschritten'
WHEN 5 THEN 'experte'
ELSE NULL
END,
ALTER COLUMN target_level TYPE VARCHAR(20) USING
CASE target_level
WHEN 0 THEN NULL
WHEN 1 THEN 'einsteiger'
WHEN 2 THEN 'grundlagen'
WHEN 3 THEN 'aufbau'
WHEN 4 THEN 'fortgeschritten'
WHEN 5 THEN 'experte'
ELSE NULL
END,
ALTER COLUMN intensity TYPE VARCHAR(10) USING
CASE
WHEN intensity <= 3 THEN 'niedrig'
WHEN intensity <= 7 THEN 'mittel'
ELSE 'hoch'
END;
-- Neue Constraints hinzufügen
ALTER TABLE exercise_skills
ADD CONSTRAINT ck_required_level CHECK (required_level IN ('einsteiger', 'grundlagen', 'aufbau', 'fortgeschritten', 'experte')),
ADD CONSTRAINT ck_target_level CHECK (target_level IN ('einsteiger', 'grundlagen', 'aufbau', 'fortgeschritten', 'experte')),
ADD CONSTRAINT ck_intensity CHECK (intensity IN ('niedrig', 'mittel', 'hoch'));
-- KI-Tracking Feld
ALTER TABLE exercises
ADD COLUMN IF NOT EXISTS summary_ai_generated BOOLEAN DEFAULT false;
ALTER TABLE exercise_skills
ADD COLUMN IF NOT EXISTS ai_suggested BOOLEAN DEFAULT false;
```
**exercise_training_characters:**
```sql
CREATE TABLE exercise_training_characters (
id SERIAL PRIMARY KEY,
exercise_id INT NOT NULL REFERENCES exercises(id) ON DELETE CASCADE,
training_character_id INT NOT NULL REFERENCES training_characters(id) ON DELETE RESTRICT,
is_primary BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(exercise_id, training_character_id)
);
```
### 6.5 Exercise Blocks (Migration 017)
```sql
CREATE TABLE exercise_blocks (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
goal TEXT,
is_template BOOLEAN DEFAULT false,
club_id INT REFERENCES clubs(id) ON DELETE SET NULL,
created_by INT REFERENCES profiles(id) ON DELETE SET NULL,
visibility VARCHAR(20) DEFAULT 'private' CHECK (visibility IN ('private', 'club', 'official')),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE exercise_block_items (
id SERIAL PRIMARY KEY,
block_id INT NOT NULL REFERENCES exercise_blocks(id) ON DELETE CASCADE,
exercise_id INT REFERENCES exercises(id) ON DELETE RESTRICT,
variant_id INT REFERENCES exercise_variants(id) ON DELETE SET NULL,
sequence_order INT NOT NULL,
is_placeholder BOOLEAN DEFAULT false,
placeholder_criteria JSONB,
placeholder_label VARCHAR(100),
notes TEXT,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(block_id, sequence_order),
CHECK (
(is_placeholder = false AND exercise_id IS NOT NULL) OR
(is_placeholder = true AND exercise_id IS NULL)
)
);
```
### 6.6 Saved Searches (Migration 016)
```sql
CREATE TABLE saved_exercise_searches (
id SERIAL PRIMARY KEY,
profile_id INT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
filters JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
---
## 6. Indizes (Komplett-Übersicht)
```sql
-- Volltext-Suche
CREATE INDEX idx_exercises_search ON exercises USING gin(search_vector);
-- Semantic Matching (optional)
CREATE INDEX idx_exercises_embedding ON exercises USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
-- Häufige Filter
CREATE INDEX idx_exercises_visibility ON exercises(visibility);
CREATE INDEX idx_exercises_status ON exercises(status);
CREATE INDEX idx_exercises_created_at ON exercises(created_at DESC);
CREATE INDEX idx_exercises_club ON exercises(club_id);
CREATE INDEX idx_exercises_creator ON exercises(created_by);
-- M:N Relations
CREATE INDEX idx_exercise_focus_areas_exercise ON exercise_focus_areas(exercise_id);
CREATE INDEX idx_exercise_focus_areas_focus ON exercise_focus_areas(focus_area_id);
CREATE INDEX idx_exercise_styles_exercise ON exercise_training_styles(exercise_id);
CREATE INDEX idx_exercise_styles_style ON exercise_training_styles(training_style_id);
CREATE INDEX idx_exercise_target_groups_exercise ON exercise_target_groups(exercise_id);
CREATE INDEX idx_exercise_target_groups_group ON exercise_target_groups(target_group_id);
CREATE INDEX idx_exercise_skills_exercise ON exercise_skills(exercise_id);
CREATE INDEX idx_exercise_skills_skill ON exercise_skills(skill_id);
CREATE INDEX idx_exercise_characters_exercise ON exercise_training_characters(exercise_id);
CREATE INDEX idx_exercise_characters_character ON exercise_training_characters(training_character_id);
-- Variants
CREATE INDEX idx_exercise_variants_exercise ON exercise_variants(exercise_id);
CREATE INDEX idx_exercise_variants_prerequisite ON exercise_variants(prerequisite_variant_id);
-- Media
CREATE INDEX idx_exercise_media_exercise ON exercise_media(exercise_id);
CREATE INDEX idx_exercise_media_primary ON exercise_media(is_primary) WHERE is_primary = true;
CREATE INDEX idx_exercise_media_context ON exercise_media(context);
-- Saved Searches
CREATE INDEX idx_saved_searches_profile ON saved_exercise_searches(profile_id);
-- Exercise Blocks
CREATE INDEX idx_exercise_blocks_club ON exercise_blocks(club_id);
CREATE INDEX idx_exercise_blocks_creator ON exercise_blocks(created_by);
CREATE INDEX idx_exercise_blocks_visibility ON exercise_blocks(visibility);
CREATE INDEX idx_exercise_blocks_template ON exercise_blocks(is_template) WHERE is_template = true;
CREATE INDEX idx_exercise_block_items_block ON exercise_block_items(block_id);
CREATE INDEX idx_exercise_block_items_exercise ON exercise_block_items(exercise_id);
CREATE INDEX idx_exercise_block_items_placeholder ON exercise_block_items(is_placeholder) WHERE is_placeholder = true;
```
---
## 7. Constraints & Business Rules
### 7.1 Data Integrity
**Exercises:**
- `title` mindestens 3 Zeichen
- `goal` mindestens 10 Zeichen
- `execution` mindestens 10 Zeichen
- `duration_min <= duration_max`
- `group_size_min <= group_size_max`
**Variants:**
- `variant_name` mindestens 3 Zeichen
- `prerequisite_variant_id` muss zum gleichen Exercise gehören
**Media:**
- Max. 50 MB per file (enforced in Backend)
- Max. 10 media items per exercise (enforced in Backend)
- Entweder `file_path` ODER `embed_url` (enforced in CHECK)
### 7.2 Cascading Delete
**ON DELETE CASCADE:**
- exercise → variants
- exercise → media
- exercise → M:N relations (focus_areas, styles, etc.)
- profile → saved searches
**ON DELETE RESTRICT:**
- focus_area → exercise_focus_areas (verhindert Löschen genutzter Katalog-Einträge)
- skill → exercise_skills
- target_group → exercise_target_groups
**ON DELETE SET NULL:**
- profile (creator) → exercises
- club → exercises
- variant (prerequisite) → variants
---
## 8. Trigger-Funktionen
### 8.1 Search Vector Auto-Update
```sql
CREATE OR REPLACE FUNCTION update_exercises_search_vector()
RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('german', COALESCE(NEW.title, '')), 'A') ||
setweight(to_tsvector('german', COALESCE(NEW.summary, '')), 'B') ||
setweight(to_tsvector('german', COALESCE(NEW.execution, '')), 'C') ||
setweight(to_tsvector('german', COALESCE(NEW.trainer_notes, '')), 'D');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER exercises_search_update
BEFORE INSERT OR UPDATE ON exercises
FOR EACH ROW EXECUTE FUNCTION update_exercises_search_vector();
```
### 8.2 Updated_At Timestamp
```sql
CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS trigger AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger für alle relevanten Tabellen
CREATE TRIGGER exercises_update_timestamp
BEFORE UPDATE ON exercises
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
CREATE TRIGGER exercise_variants_update_timestamp
BEFORE UPDATE ON exercise_variants
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
CREATE TRIGGER exercise_media_update_timestamp
BEFORE UPDATE ON exercise_media
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
CREATE TRIGGER saved_searches_update_timestamp
BEFORE UPDATE ON saved_exercise_searches
FOR EACH ROW EXECUTE FUNCTION update_timestamp();
```
---
## 9. Migrations-Tracking
**Tabelle: schema_migrations**
```sql
CREATE TABLE IF NOT EXISTS schema_migrations (
migration_id VARCHAR(50) PRIMARY KEY,
description TEXT,
applied_at TIMESTAMP DEFAULT NOW()
);
```
**Nach jeder Migration:**
```sql
INSERT INTO schema_migrations (migration_id, description) VALUES
('014', 'Variant Progression System + Search Vector + Legacy-Cleanup'),
('015', 'Semantic Exercise Matching (OPTIONAL)'),
('016', 'Saved Exercise Searches'),
('017', 'Exercise Blocks + Template Blocks');
```
---
## 10. Datenbank-Größenabschätzung
**Annahmen:**
- 5.000 Übungen
- Durchschnittlich 2 Varianten pro Übung
- Durchschnittlich 3 Media-Items pro Übung
- Durchschnittlich 5 M:N Zuordnungen pro Übung
**Geschätzte Zeilen:**
```
exercises: 5.000
exercise_variants: 10.000
exercise_media: 15.000
exercise_focus_areas: 10.000
exercise_training_styles: 10.000
exercise_target_groups: 10.000
exercise_skills: 25.000
exercise_blocks: 1.000
exercise_block_items: 5.000
─────────────────────────────────────
TOTAL: 101.000 Zeilen
```
**Speicherbedarf (ohne Media-Files):**
- exercises Tabelle: ~50 MB (mit search_vector + embedding)
- Alle M:N Tabellen: ~10 MB
- Variants + Media: ~5 MB
- **Total DB:** ~65 MB
**Media-Files (lokal gespeichert):**
- 15.000 Items × 5 MB Durchschnitt = ~75 GB
---
## 11. Performance-Benchmarks
**Target Query Performance:**
- List Exercises (ohne Filter): < 50ms
- List Exercises (mit 3 Filtern): < 100ms
- Volltext-Suche: < 150ms
- Exercise Detail (enriched): < 80ms
- Semantic Similarity: < 200ms (optional)
**Optimierungen:**
- GIN-Index für tsvector 10x schnellere Suche
- IVFFlat-Index für Embeddings 50x schnellere Similarity-Search
- Partial Index für `is_primary` Media schnellere Primary-Lookups
---
**Version:** 1.0
**Letzte Änderung:** 2026-04-24
**Status:** DRAFT - Awaiting Review