// Package drivers owns persistence and business logic for drivers // (racers). Each driver has a unique 3-letter nickname (A-Z, // uppercase, exactly 3 chars), a name, an optional avatar URL and an // optional clan reference. package drivers import ( "context" "errors" "fmt" "regexp" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // Errors returned by the service. var ( ErrNotFound = errors.New("driver not found") ErrInvalidInput = errors.New("invalid input") ErrAlreadyExists = errors.New("driver already exists") ) // nicknameRE enforces exactly 3 uppercase ASCII letters. var nicknameRE = regexp.MustCompile(`^[A-Z]{3}$`) // IsValidNickname reports whether s is a valid driver nickname. func IsValidNickname(s string) bool { return nicknameRE.MatchString(s) } // Driver is the canonical in-memory representation. type Driver struct { ID string Nickname string Name string AvatarURL string ClanID string CreatedMs int64 UpdatedMs int64 } // PgStore is the Postgres-backed half of the package. type PgStore struct { pool *pgxpool.Pool } // NewPgStore wraps an open pgxpool. func NewPgStore(pool *pgxpool.Pool) *PgStore { return &PgStore{pool: pool} } // Exec exposes raw SQL for the seeder. func (s *PgStore) Exec(ctx context.Context, sql string) (int64, error) { tag, err := s.pool.Exec(ctx, sql) if err != nil { return 0, err } return tag.RowsAffected(), nil } // Create inserts a driver. func (s *PgStore) Create(ctx context.Context, d Driver) error { now := time.Now().UnixMilli() if d.CreatedMs == 0 { d.CreatedMs = now } d.UpdatedMs = now _, err := s.pool.Exec(ctx, ` INSERT INTO drivers (id, nickname, name, avatar_url, clan_id, created_ms, updated_ms) VALUES ($1, $2, $3, $4, $5, $6, $7)`, d.ID, d.Nickname, d.Name, d.AvatarURL, nullableClan(d.ClanID), d.CreatedMs, d.UpdatedMs) if err != nil { return fmt.Errorf("insert driver: %w", err) } return nil } func nullableClan(id string) any { if id == "" { return nil } return id } // Get loads a driver by id. func (s *PgStore) Get(ctx context.Context, id string) (Driver, error) { row := s.pool.QueryRow(ctx, `SELECT id, nickname, name, avatar_url, COALESCE(clan_id, ''), created_ms, updated_ms FROM drivers WHERE id = $1`, id) var d Driver if err := row.Scan(&d.ID, &d.Nickname, &d.Name, &d.AvatarURL, &d.ClanID, &d.CreatedMs, &d.UpdatedMs); err != nil { if errors.Is(err, pgx.ErrNoRows) { return Driver{}, ErrNotFound } return Driver{}, err } return d, nil } // GetByNickname loads a driver by their 3-letter nickname. func (s *PgStore) GetByNickname(ctx context.Context, nick string) (Driver, error) { row := s.pool.QueryRow(ctx, `SELECT id, nickname, name, avatar_url, COALESCE(clan_id, ''), created_ms, updated_ms FROM drivers WHERE nickname = $1`, nick) var d Driver if err := row.Scan(&d.ID, &d.Nickname, &d.Name, &d.AvatarURL, &d.ClanID, &d.CreatedMs, &d.UpdatedMs); err != nil { if errors.Is(err, pgx.ErrNoRows) { return Driver{}, ErrNotFound } return Driver{}, err } return d, nil } // List returns all drivers, ordered by nickname. func (s *PgStore) List(ctx context.Context, limit, offset int, clanID string) ([]Driver, error) { if limit <= 0 || limit > 200 { limit = 50 } if offset < 0 { offset = 0 } q := `SELECT id, nickname, name, avatar_url, COALESCE(clan_id, ''), created_ms, updated_ms FROM drivers` args := []any{} if clanID != "" { q += ` WHERE clan_id = $1` args = append(args, clanID) args = append(args, limit, offset) q += ` ORDER BY nickname ASC LIMIT $2 OFFSET $3` } else { args = append(args, limit, offset) q += ` ORDER BY nickname ASC LIMIT $1 OFFSET $2` } rows, err := s.pool.Query(ctx, q, args...) if err != nil { return nil, err } defer rows.Close() out := make([]Driver, 0) for rows.Next() { var d Driver if err := rows.Scan(&d.ID, &d.Nickname, &d.Name, &d.AvatarURL, &d.ClanID, &d.CreatedMs, &d.UpdatedMs); err != nil { return nil, err } out = append(out, d) } return out, rows.Err() } // Update applies a partial patch. func (s *PgStore) Update(ctx context.Context, d Driver) error { d.UpdatedMs = time.Now().UnixMilli() tag, err := s.pool.Exec(ctx, ` UPDATE drivers SET name = $2, avatar_url = $3, clan_id = $4, updated_ms = $5 WHERE id = $1`, d.ID, d.Name, d.AvatarURL, nullableClan(d.ClanID), d.UpdatedMs) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } // Delete removes a driver by id. func (s *PgStore) Delete(ctx context.Context, id string) error { tag, err := s.pool.Exec(ctx, `DELETE FROM drivers WHERE id = $1`, id) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } // Count returns the total number of drivers. func (s *PgStore) Count(ctx context.Context) (int, error) { var n int row := s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM drivers`) if err := row.Scan(&n); err != nil { return 0, err } return n, nil }