911 lines
30 KiB
Markdown
911 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)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 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
|