You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

255 lines
5.5 KiB

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 a list of heats for a group
func (db *DB) SaveHeats(groupID int64, heats []models.Heat) error {
// Start a transaction
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// Delete existing heats for this group
_, err = tx.Exec("DELETE FROM heats WHERE group_id = ?", groupID)
if err != nil {
return 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 err
}
defer stmt.Close()
for _, heat := range heats {
_, err = stmt.Exec(
groupID,
heat.HeatNum,
nullableInt64(heat.Lane1ID),
nullableInt64(heat.Lane2ID),
nullableInt64(heat.Lane3ID),
nullableInt64(heat.Lane4ID),
)
if err != nil {
return err
}
}
// Commit the transaction
return tx.Commit()
}
// Helper function to handle nullable int64 values
func nullableInt64(i *int64) interface{} {
if i == nil {
return nil
}
return *i
}
// GetHeats retrieves all heats for a group
func (db *DB) GetHeats(groupID int64) ([]models.Heat, error) {
rows, err := db.Query(`
SELECT id, group_id, 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, err
}
defer rows.Close()
var heats []models.Heat
for rows.Next() {
var heat models.Heat
var lane1, lane2, lane3, lane4 sql.NullInt64
err := rows.Scan(
&heat.ID,
&heat.GroupID,
&heat.HeatNum,
&lane1,
&lane2,
&lane3,
&lane4,
)
if err != nil {
return nil, err
}
if lane1.Valid {
val := lane1.Int64
heat.Lane1ID = &val
}
if lane2.Valid {
val := lane2.Int64
heat.Lane2ID = &val
}
if lane3.Valid {
val := lane3.Int64
heat.Lane3ID = &val
}
if lane4.Valid {
val := lane4.Int64
heat.Lane4ID = &val
}
heats = append(heats, heat)
}
return heats, 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
}