- 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.
913 lines
30 KiB
Markdown
913 lines
30 KiB
Markdown
# 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 (einsteiger–experte), 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 **032–034** — 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
|