354 lines
16 KiB
SQL
354 lines
16 KiB
SQL
-- Fitness Tracking App - Production Database Schema
|
|
-- Database: fitness_app
|
|
-- Created: 2026-04-21
|
|
|
|
-- Create Database
|
|
CREATE DATABASE IF NOT EXISTS fitness_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
USE fitness_app;
|
|
|
|
-- Users Table
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
uuid CHAR(36) UNIQUE NOT NULL COMMENT 'Unique identifier for the user',
|
|
phone_e164 VARCHAR(16) UNIQUE COMMENT 'Verified phone number in E.164 format',
|
|
phone_verified_at DATETIME NULL,
|
|
username VARCHAR(50) UNIQUE,
|
|
email VARCHAR(100) UNIQUE,
|
|
password_hash VARCHAR(255) NULL COMMENT 'Optional legacy password hash',
|
|
api_key VARCHAR(64) UNIQUE COMMENT 'Optional legacy API key',
|
|
api_secret VARCHAR(64) NULL COMMENT 'Optional legacy HMAC secret',
|
|
full_name VARCHAR(100),
|
|
avatar_url VARCHAR(255),
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
account_role ENUM('member', 'owner', 'content_manager', 'support') NOT NULL DEFAULT 'member',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
INDEX idx_uuid (uuid),
|
|
INDEX idx_api_key (api_key),
|
|
INDEX idx_created_at (created_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
CREATE TABLE app_settings (
|
|
setting_key VARCHAR(100) PRIMARY KEY,
|
|
setting_value JSON NOT NULL,
|
|
is_public BOOLEAN NOT NULL DEFAULT FALSE,
|
|
revision BIGINT UNSIGNED NOT NULL DEFAULT 1,
|
|
updated_by INT NULL,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
INDEX idx_settings_public (is_public, setting_key),
|
|
CONSTRAINT fk_settings_editor FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
CREATE TABLE app_setting_audit (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
setting_key VARCHAR(100) NOT NULL,
|
|
previous_value JSON NULL,
|
|
new_value JSON NOT NULL,
|
|
actor_user_id INT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_settings_audit_key_time (setting_key, created_at),
|
|
INDEX idx_settings_audit_actor_time (actor_user_id, created_at),
|
|
CONSTRAINT fk_settings_audit_actor FOREIGN KEY (actor_user_id) REFERENCES users(id) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- Curated workout library. GIFs are optional, versioned media URLs, not uploaded inline.
|
|
CREATE TABLE exercises (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
exercise_uuid CHAR(36) NOT NULL UNIQUE,
|
|
slug VARCHAR(100) NOT NULL UNIQUE,
|
|
title_ar VARCHAR(160) NOT NULL,
|
|
title_en VARCHAR(160) NULL,
|
|
instructions_ar JSON NOT NULL,
|
|
target_muscles JSON NOT NULL,
|
|
equipment JSON NOT NULL,
|
|
difficulty ENUM('beginner', 'intermediate', 'advanced') NOT NULL DEFAULT 'beginner',
|
|
movement_type ENUM('strength', 'mobility', 'cardio', 'recovery') NOT NULL,
|
|
gif_url VARCHAR(500) NULL,
|
|
gif_poster_url VARCHAR(500) NULL,
|
|
duration_seconds SMALLINT UNSIGNED NULL,
|
|
repetitions VARCHAR(80) NULL,
|
|
safety_notes_ar JSON NOT NULL,
|
|
alternative_exercise_id BIGINT UNSIGNED NULL,
|
|
is_published BOOLEAN NOT NULL DEFAULT FALSE,
|
|
content_revision INT UNSIGNED NOT NULL DEFAULT 1,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
INDEX idx_exercises_public (is_published, movement_type, difficulty),
|
|
CONSTRAINT fk_exercise_alternative FOREIGN KEY (alternative_exercise_id) REFERENCES exercises(id) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
CREATE TABLE training_plans (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
plan_uuid CHAR(36) NOT NULL UNIQUE,
|
|
slug VARCHAR(100) NOT NULL UNIQUE,
|
|
title_ar VARCHAR(160) NOT NULL,
|
|
summary_ar TEXT NOT NULL,
|
|
goal ENUM('general_fitness', 'weight_management', 'mobility', 'endurance') NOT NULL,
|
|
level ENUM('beginner', 'intermediate', 'advanced') NOT NULL,
|
|
weeks_duration TINYINT UNSIGNED NOT NULL,
|
|
source_notes JSON NULL,
|
|
safety_notes_ar JSON NOT NULL,
|
|
is_published BOOLEAN NOT NULL DEFAULT FALSE,
|
|
content_revision INT UNSIGNED NOT NULL DEFAULT 1,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
INDEX idx_plans_public (is_published, goal, level)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
CREATE TABLE training_plan_sessions (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
plan_id BIGINT UNSIGNED NOT NULL,
|
|
week_number TINYINT UNSIGNED NOT NULL,
|
|
day_number TINYINT UNSIGNED NOT NULL,
|
|
title_ar VARCHAR(160) NOT NULL,
|
|
session_type ENUM('strength', 'walking', 'mobility', 'rest') NOT NULL,
|
|
duration_minutes SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
|
intensity ENUM('easy', 'moderate', 'vigorous') NOT NULL DEFAULT 'easy',
|
|
notes_ar TEXT NULL,
|
|
sort_order SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
|
UNIQUE KEY uq_plan_week_day (plan_id, week_number, day_number),
|
|
CONSTRAINT fk_plan_sessions_plan FOREIGN KEY (plan_id) REFERENCES training_plans(id) ON DELETE CASCADE,
|
|
INDEX idx_plan_sessions_order (plan_id, week_number, sort_order)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
CREATE TABLE training_session_exercises (
|
|
session_id BIGINT UNSIGNED NOT NULL,
|
|
exercise_id BIGINT UNSIGNED NOT NULL,
|
|
sort_order SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
|
sets TINYINT UNSIGNED NULL,
|
|
reps VARCHAR(60) NULL,
|
|
duration_seconds SMALLINT UNSIGNED NULL,
|
|
rest_seconds SMALLINT UNSIGNED NOT NULL DEFAULT 45,
|
|
PRIMARY KEY (session_id, exercise_id),
|
|
CONSTRAINT fk_session_exercises_session FOREIGN KEY (session_id) REFERENCES training_plan_sessions(id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_session_exercises_exercise FOREIGN KEY (exercise_id) REFERENCES exercises(id) ON DELETE RESTRICT
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
CREATE TABLE training_content_audit (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
entity_type ENUM('exercise', 'plan') NOT NULL,
|
|
entity_uuid CHAR(36) NOT NULL,
|
|
action ENUM('create', 'update', 'publish', 'unpublish') NOT NULL,
|
|
previous_value JSON NULL,
|
|
new_value JSON NOT NULL,
|
|
actor_user_id INT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_content_audit_entity_time (entity_type, entity_uuid, created_at),
|
|
INDEX idx_content_audit_actor_time (actor_user_id, created_at),
|
|
CONSTRAINT fk_content_audit_actor FOREIGN KEY (actor_user_id)
|
|
REFERENCES users(id) ON DELETE SET NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
CREATE TABLE meal_entries (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
user_id INT NOT NULL,
|
|
client_meal_uuid CHAR(36) NOT NULL,
|
|
name VARCHAR(180) NOT NULL,
|
|
meal_type ENUM('breakfast', 'lunch', 'dinner', 'snack') NOT NULL,
|
|
calories DECIMAL(8,2) NOT NULL,
|
|
protein_grams DECIMAL(7,2) NOT NULL DEFAULT 0,
|
|
carbohydrate_grams DECIMAL(7,2) NOT NULL DEFAULT 0,
|
|
fat_grams DECIMAL(7,2) NOT NULL DEFAULT 0,
|
|
consumed_at DATETIME NOT NULL,
|
|
source ENUM('manual', 'photo_ai') NOT NULL DEFAULT 'manual',
|
|
notes VARCHAR(1000) NOT NULL DEFAULT '',
|
|
revision INT UNSIGNED NOT NULL DEFAULT 1,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
UNIQUE KEY uq_meal_user_client_uuid (user_id, client_meal_uuid),
|
|
INDEX idx_meals_user_consumed (user_id, consumed_at DESC),
|
|
CONSTRAINT fk_meals_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
CHECK (calories >= 0 AND calories <= 10000),
|
|
CHECK (protein_grams >= 0 AND protein_grams <= 1000),
|
|
CHECK (carbohydrate_grams >= 0 AND carbohydrate_grams <= 1000),
|
|
CHECK (fat_grams >= 0 AND fat_grams <= 1000)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- Phone OTP challenges contain keyed digests, never the plaintext code.
|
|
CREATE TABLE otp_challenges (
|
|
challenge_uuid CHAR(36) PRIMARY KEY,
|
|
phone_e164 VARCHAR(16) NOT NULL,
|
|
purpose ENUM('register', 'login', 'change_phone') NOT NULL,
|
|
code_digest CHAR(64) NOT NULL,
|
|
attempt_count TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
|
max_attempts TINYINT UNSIGNED NOT NULL DEFAULT 5,
|
|
request_ip_digest CHAR(64) NULL,
|
|
device_uuid CHAR(36) NULL,
|
|
expires_at DATETIME NOT NULL,
|
|
consumed_at DATETIME NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_otp_phone_created (phone_e164, created_at),
|
|
INDEX idx_otp_expiry (expires_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
CREATE TABLE auth_rate_limit_buckets (
|
|
bucket_digest CHAR(64) PRIMARY KEY,
|
|
bucket_type ENUM('phone', 'ip', 'device') NOT NULL,
|
|
window_started_at DATETIME NOT NULL,
|
|
request_count INT UNSIGNED NOT NULL DEFAULT 0,
|
|
blocked_until DATETIME NULL,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
INDEX idx_rate_bucket_expiry (updated_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
CREATE TABLE user_devices (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
user_id INT NOT NULL,
|
|
device_uuid CHAR(36) NOT NULL,
|
|
platform ENUM('ios', 'android', 'web') NOT NULL,
|
|
public_key TEXT NULL,
|
|
key_fingerprint CHAR(64) NULL UNIQUE,
|
|
key_algorithm VARCHAR(32) NULL,
|
|
display_name VARCHAR(80) NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
last_seen_at DATETIME NULL,
|
|
revoked_at DATETIME NULL,
|
|
UNIQUE KEY uq_device_user_uuid (user_id, device_uuid),
|
|
INDEX idx_devices_user_active (user_id, revoked_at),
|
|
CONSTRAINT fk_devices_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
CREATE TABLE auth_sessions (
|
|
session_uuid CHAR(36) PRIMARY KEY,
|
|
family_uuid CHAR(36) NOT NULL,
|
|
user_id INT NOT NULL,
|
|
device_uuid CHAR(36) NULL,
|
|
refresh_token_digest CHAR(64) NOT NULL UNIQUE,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
last_used_at DATETIME NULL,
|
|
expires_at DATETIME NOT NULL,
|
|
revoked_at DATETIME NULL,
|
|
replaced_by CHAR(36) NULL,
|
|
INDEX idx_sessions_user_active (user_id, revoked_at, expires_at),
|
|
INDEX idx_sessions_family (family_uuid),
|
|
CONSTRAINT fk_sessions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_sessions_device FOREIGN KEY (user_id, device_uuid)
|
|
REFERENCES user_devices(user_id, device_uuid) ON DELETE CASCADE
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- Workouts Table
|
|
CREATE TABLE IF NOT EXISTS workouts (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
workout_uuid CHAR(36) UNIQUE NOT NULL COMMENT 'Unique identifier for the workout',
|
|
user_id INT NOT NULL,
|
|
client_workout_uuid CHAR(36) NULL COMMENT 'Client-generated idempotency key',
|
|
workout_type ENUM('running', 'walking') NOT NULL,
|
|
distance_meters INT NOT NULL COMMENT 'Total distance in meters',
|
|
duration_seconds INT NOT NULL COMMENT 'Total duration in seconds',
|
|
elevation_gain_meters INT DEFAULT 0 COMMENT 'Elevation gain in meters',
|
|
elevation_loss_meters INT DEFAULT 0 COMMENT 'Elevation loss in meters',
|
|
calories_burned FLOAT DEFAULT 0,
|
|
average_pace_mps FLOAT COMMENT 'Average pace in meters per second',
|
|
max_speed_mps FLOAT DEFAULT 0 COMMENT 'Max speed in meters per second',
|
|
route_polyline LONGTEXT NOT NULL COMMENT 'Encoded polyline format (Google Maps Polyline Algorithm)',
|
|
coordinate_count INT NOT NULL COMMENT 'Total number of GPS coordinates recorded',
|
|
start_lat DECIMAL(10, 8) NOT NULL,
|
|
start_lng DECIMAL(11, 8) NOT NULL,
|
|
end_lat DECIMAL(10, 8) NOT NULL,
|
|
end_lng DECIMAL(11, 8) NOT NULL,
|
|
start_time DATETIME NOT NULL COMMENT 'ISO 8601 datetime when workout started',
|
|
end_time DATETIME NOT NULL COMMENT 'ISO 8601 datetime when workout ended',
|
|
weather_condition VARCHAR(50),
|
|
temperature_celsius FLOAT,
|
|
notes TEXT,
|
|
is_public BOOLEAN DEFAULT FALSE,
|
|
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
INDEX idx_workout_uuid (workout_uuid),
|
|
INDEX idx_user_id (user_id),
|
|
INDEX idx_workout_type (workout_type),
|
|
INDEX idx_synced_at (synced_at),
|
|
INDEX idx_created_at (created_at),
|
|
INDEX idx_user_created (user_id, created_at),
|
|
UNIQUE KEY uq_user_client_workout (user_id, client_workout_uuid)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- Workout Segments Table (for detailed route tracking if needed)
|
|
CREATE TABLE IF NOT EXISTS workout_segments (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
workout_id INT NOT NULL,
|
|
segment_order INT NOT NULL COMMENT 'Order of segment in workout',
|
|
duration_seconds INT,
|
|
distance_meters INT,
|
|
average_pace_mps FLOAT,
|
|
index_in_polyline INT COMMENT 'Start index in polyline',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (workout_id) REFERENCES workouts(id) ON DELETE CASCADE,
|
|
INDEX idx_workout_id (workout_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- API Logs Table (for debugging and analytics)
|
|
CREATE TABLE IF NOT EXISTS api_logs (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
user_id INT,
|
|
endpoint VARCHAR(255),
|
|
method VARCHAR(10),
|
|
status_code INT,
|
|
request_hash VARCHAR(64) COMMENT 'Hash of request for deduplication',
|
|
ip_address VARCHAR(45),
|
|
user_agent VARCHAR(255),
|
|
response_time_ms INT,
|
|
error_message TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_user_id (user_id),
|
|
INDEX idx_created_at (created_at),
|
|
INDEX idx_endpoint (endpoint),
|
|
INDEX idx_request_hash (request_hash)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- Statistics Cache Table (for performance optimization)
|
|
CREATE TABLE IF NOT EXISTS user_stats_cache (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
user_id INT UNIQUE NOT NULL,
|
|
total_workouts INT DEFAULT 0,
|
|
total_distance_meters INT DEFAULT 0,
|
|
total_duration_seconds INT DEFAULT 0,
|
|
total_calories_burned FLOAT DEFAULT 0,
|
|
average_pace_mps FLOAT,
|
|
last_workout_date DATETIME,
|
|
streak_days INT DEFAULT 0,
|
|
cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
INDEX idx_user_id (user_id),
|
|
INDEX idx_cached_at (cached_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
|
|
-- Create Triggers for audit trail
|
|
DELIMITER $$
|
|
|
|
CREATE TRIGGER workout_audit_insert AFTER INSERT ON workouts
|
|
FOR EACH ROW
|
|
BEGIN
|
|
INSERT INTO api_logs (endpoint, method, status_code, error_message, created_at)
|
|
VALUES ('workouts', 'INSERT', 201, NULL, NOW());
|
|
END $$
|
|
|
|
DELIMITER ;
|
|
|
|
-- Stored Procedure to get user workout statistics
|
|
DELIMITER $$
|
|
|
|
CREATE PROCEDURE GetUserStats(IN p_user_id INT)
|
|
BEGIN
|
|
SELECT
|
|
COUNT(*) as total_workouts,
|
|
SUM(distance_meters) as total_distance,
|
|
SUM(duration_seconds) as total_duration,
|
|
SUM(calories_burned) as total_calories,
|
|
AVG(average_pace_mps) as avg_pace,
|
|
MAX(created_at) as last_workout,
|
|
workout_type
|
|
FROM workouts
|
|
WHERE user_id = p_user_id
|
|
GROUP BY workout_type
|
|
ORDER BY created_at DESC;
|
|
END $$
|
|
|
|
DELIMITER ;
|
|
|
|
-- Initial indexes for optimal query performance
|
|
CREATE INDEX idx_workouts_stats ON workouts(user_id, workout_type, created_at);
|
|
CREATE INDEX idx_users_active ON users(is_active, created_at);
|