package db import ( "database/sql" "fmt" "log/slog" "os" "path/filepath" "track-gopher/models" _ "github.com/mattn/go-sqlite3" ) // DB represents the database connection type DB struct { *sql.DB logger *slog.Logger } // NewDB creates a new database connection func New(dbPath string) (*DB, error) { logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelDebug, })) // Ensure directory exists if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { return nil, fmt.Errorf("failed to create database directory: %w", err) } // Open database connection sqlDB, err := sql.Open("sqlite3", dbPath) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } // Create database instance db := &DB{ DB: sqlDB, logger: logger, } // Initialize schema if err := db.initSchema(); err != nil { db.Close() return nil, fmt.Errorf("failed to initialize schema: %w", err) } return db, nil } // initSchema creates the database tables if they don't exist func (db *DB) initSchema() error { // Create groups table _, err := db.Exec(` CREATE TABLE IF NOT EXISTS groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `) if err != nil { return fmt.Errorf("failed to create groups table: %w", err) } // Create racers table _, err = db.Exec(` CREATE TABLE IF NOT EXISTS racers ( id INTEGER PRIMARY KEY AUTOINCREMENT, first_name TEXT NOT NULL, last_name TEXT NOT NULL, car_number TEXT NOT NULL UNIQUE, car_weight REAL NOT NULL, group_id INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (group_id) REFERENCES groups(id) ) `) if err != nil { return fmt.Errorf("failed to create racers table: %w", err) } // Create races table _, err = db.Exec(` CREATE TABLE IF NOT EXISTS races ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `) if err != nil { return fmt.Errorf("failed to create races table: %w", err) } // Create race_results table _, err = db.Exec(` CREATE TABLE IF NOT EXISTS race_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, race_id INTEGER NOT NULL, racer_id INTEGER NOT NULL, lane INTEGER NOT NULL, time REAL, place INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (race_id) REFERENCES races(id), FOREIGN KEY (racer_id) REFERENCES racers(id), UNIQUE(race_id, lane) ) `) if err != nil { return fmt.Errorf("failed to create race_results table: %w", err) } // Create heats table _, err = db.Exec(` CREATE TABLE IF NOT EXISTS heats ( id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL, heat_num INTEGER NOT NULL, lane1_id INTEGER, lane2_id INTEGER, lane3_id INTEGER, lane4_id INTEGER, FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE, FOREIGN KEY (lane1_id) REFERENCES racers(id) ON DELETE SET NULL, FOREIGN KEY (lane2_id) REFERENCES racers(id) ON DELETE SET NULL, FOREIGN KEY (lane3_id) REFERENCES racers(id) ON DELETE SET NULL, FOREIGN KEY (lane4_id) REFERENCES racers(id) ON DELETE SET NULL ) `) if err != nil { return fmt.Errorf("failed to create heats table: %w", err) } return nil } // Close closes the database connection func (db *DB) Close() error { return db.DB.Close() } // SaveHeats saves the heats for a group, replacing any existing heats func (db *DB) SaveHeats(groupID int64, heats []models.Heat) error { // Start a transaction tx, err := db.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete existing heats for this group _, err = tx.Exec("DELETE FROM heats WHERE group_id = ?", groupID) if err != nil { return fmt.Errorf("failed to delete existing heats: %w", err) } // Insert new heats stmt, err := tx.Prepare("INSERT INTO heats (group_id, heat_num, lane1_id, lane2_id, lane3_id, lane4_id) VALUES (?, ?, ?, ?, ?, ?)") if err != nil { return fmt.Errorf("failed to prepare statement: %w", err) } defer stmt.Close() for i, heat := range heats { _, err = stmt.Exec(groupID, i+1, heat.Lane1ID, heat.Lane2ID, heat.Lane3ID, heat.Lane4ID) if err != nil { return fmt.Errorf("failed to insert heat: %w", err) } } // Commit transaction if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } // GetHeats retrieves the saved heats for a group func (db *DB) GetHeats(groupID int64) ([]models.Heat, error) { rows, err := db.Query(` SELECT heat_num, lane1_id, lane2_id, lane3_id, lane4_id FROM heats WHERE group_id = ? ORDER BY heat_num `, groupID) if err != nil { return nil, fmt.Errorf("failed to query heats: %w", err) } defer rows.Close() var heats []models.Heat for rows.Next() { var heat models.Heat err := rows.Scan(&heat.HeatNum, &heat.Lane1ID, &heat.Lane2ID, &heat.Lane3ID, &heat.Lane4ID) if err != nil { return nil, fmt.Errorf("failed to scan heat: %w", err) } heats = append(heats, heat) } return heats, nil } // HasSavedHeats checks if a group has saved heats func (db *DB) HasSavedHeats(groupID int64) (bool, error) { var count int err := db.QueryRow("SELECT COUNT(*) FROM heats WHERE group_id = ?", groupID).Scan(&count) if err != nil { return false, fmt.Errorf("failed to check for saved heats: %w", err) } return count > 0, nil } // DeleteHeats deletes all heats for a group func (db *DB) DeleteHeats(groupID int64) error { _, err := db.Exec("DELETE FROM heats WHERE group_id = ?", groupID) return err }