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.
282 lines
6.2 KiB
282 lines
6.2 KiB
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
_ "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()
|
|
}
|
|
|
|
// Group represents a group of racers
|
|
type Group struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// Racer represents a racer in the derby
|
|
type Racer struct {
|
|
ID int64 `json:"id"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
CarNumber string `json:"car_number"`
|
|
CarWeight float64 `json:"car_weight"`
|
|
GroupID int64 `json:"group_id"`
|
|
}
|
|
|
|
// Heat represents a single race with 4 lanes
|
|
type Heat struct {
|
|
ID int64 `json:"id"`
|
|
GroupID int64 `json:"group_id"`
|
|
HeatNum int `json:"heat_num"`
|
|
Lane1ID *int64 `json:"lane1_id"`
|
|
Lane2ID *int64 `json:"lane2_id"`
|
|
Lane3ID *int64 `json:"lane3_id"`
|
|
Lane4ID *int64 `json:"lane4_id"`
|
|
}
|
|
|
|
// SaveHeats saves a list of heats for a group
|
|
func (db *DB) SaveHeats(groupID int64, heats []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) ([]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 []Heat
|
|
for rows.Next() {
|
|
var heat 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
|
|
}
|