Split d4c0bb889b docs: comprehensive documentation for all modules and handlers
- README.md: full project docs with features, API endpoints, Glicko-2 explanation
- main.rs: doc comments for all 18 HTTP handlers
- db/mod.rs: schema and migration documentation
- models/mod.rs: Player struct and Glicko-2 parameter docs
- Fixed route syntax (:id instead of {id}) for Axum 0.7 compatibility
2026-02-07 19:19:50 -05:00

144 lines
6.3 KiB
Rust

use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
use std::path::Path;
/// Creates and initializes a connection pool to the SQLite database.
///
/// This function:
/// - Creates the database file if it doesn't exist
/// - Ensures parent directories are created
/// - Configures a connection pool with max 5 connections
/// - Enables foreign key constraints for referential integrity
/// - Runs database migrations to set up the schema
///
/// # Arguments
/// * `db_path` - Path to the SQLite database file (e.g., "pickleball.db")
///
/// # Returns
/// * `Ok(SqlitePool)` - Initialized connection pool ready to use
/// * `Err(sqlx::Error)` - If connection or migration fails
pub async fn create_pool(db_path: &str) -> Result<SqlitePool, sqlx::Error> {
// Create database file if it doesn't exist
let path = Path::new(db_path);
let db_exists = path.exists();
// Ensure parent directory exists
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
// Create connection pool with correct SQLite connection string
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(&format!("sqlite://{}?mode=rwc", db_path))
.await?;
// Enable foreign keys
sqlx::query("PRAGMA foreign_keys = ON")
.execute(&pool)
.await?;
// Run migrations
run_migrations(&pool).await?;
Ok(pool)
}
/// Runs database migrations to create tables and indexes if they don't exist.
///
/// Creates the following schema:
/// - **players**: Stores player profiles with separate singles/doubles Glicko2 ratings
/// - **sessions**: Tracks play sessions with optional summaries
/// - **matches**: Individual matches within sessions (singles or doubles)
/// - **match_participants**: Records each player's performance in a match with before/after ratings
///
/// All tables include foreign keys and appropriate indexes for query performance.
/// Idempotent - safe to call multiple times.
pub async fn run_migrations(pool: &SqlitePool) -> Result<(), sqlx::Error> {
let schema = include_str!("../../migrations/001_initial_schema.sql");
// Execute each statement
let statements = vec![
"PRAGMA foreign_keys = ON",
// Players table: Stores player profiles with separate Glicko2 ratings for singles and doubles
// Each player maintains independent rating systems since skill in singles vs doubles may differ
"CREATE TABLE IF NOT EXISTS players (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
email TEXT,
-- Glicko2 Rating: Skill estimate (1500 = average)
singles_rating REAL NOT NULL DEFAULT 1500.0,
-- Glicko2 RD (Rating Deviation): Confidence in rating (lower = more confident; ~30 = highly confident)
singles_rd REAL NOT NULL DEFAULT 350.0,
-- Glicko2 Volatility: Unpredictability of performance (0.06 = starting volatility)
singles_volatility REAL NOT NULL DEFAULT 0.06,
doubles_rating REAL NOT NULL DEFAULT 1500.0,
doubles_rd REAL NOT NULL DEFAULT 350.0,
doubles_volatility REAL NOT NULL DEFAULT 0.06,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
last_played TEXT NOT NULL DEFAULT (datetime('now'))
)",
// Sessions table: Groups matches that occurred during a play session
"CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
start_time TEXT NOT NULL DEFAULT (datetime('now')),
end_time TEXT,
summary_sent BOOLEAN NOT NULL DEFAULT 0,
notes TEXT
)",
// Matches table: Individual games (singles or doubles) within a session
"CREATE TABLE IF NOT EXISTS matches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
match_type TEXT NOT NULL CHECK(match_type IN ('singles', 'doubles')),
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
team1_score INTEGER NOT NULL,
team2_score INTEGER NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
)",
// Match participants table: Records each player's participation and rating changes per match
// Stores before/after ratings to allow recalculation and audit trails
"CREATE TABLE IF NOT EXISTS match_participants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
match_id INTEGER NOT NULL,
player_id INTEGER NOT NULL,
team INTEGER NOT NULL CHECK(team IN (1, 2)),
-- Rating state before match
rating_before REAL NOT NULL,
rd_before REAL NOT NULL,
volatility_before REAL NOT NULL,
-- Rating state after Glicko2 calculation
rating_after REAL NOT NULL,
rd_after REAL NOT NULL,
volatility_after REAL NOT NULL,
-- Net change in rating from this match
rating_change REAL NOT NULL,
FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE,
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
)",
// Indexes for query performance
"CREATE INDEX IF NOT EXISTS idx_matches_session ON matches(session_id)",
"CREATE INDEX IF NOT EXISTS idx_matches_timestamp ON matches(timestamp DESC)",
"CREATE INDEX IF NOT EXISTS idx_participants_match ON match_participants(match_id)",
"CREATE INDEX IF NOT EXISTS idx_participants_player ON match_participants(player_id)",
"CREATE INDEX IF NOT EXISTS idx_players_name ON players(name)",
"CREATE INDEX IF NOT EXISTS idx_players_singles_rating ON players(singles_rating DESC)",
"CREATE INDEX IF NOT EXISTS idx_players_doubles_rating ON players(doubles_rating DESC)",
];
for statement in &statements {
if !statement.trim().is_empty() {
match sqlx::query(statement).execute(pool).await {
Ok(_) => {},
Err(e) => {
// Ignore "table already exists" errors
if !e.to_string().contains("already exists") {
eprintln!("Migration error: {}", e);
}
}
}
}
}
Ok(())
}