refactor: adopt portigo infrastructure patterns
Add config package with build-tag-switched dev/prod environments, structured logging via zerolog, Taskfile for dev workflow, golangci-lint config, testutil package, and improved DB setup with proper SQLite pragmas and cleanup. Rename sqlc output package from gen to repository. Switch to allowlist .gitignore, Alpine+UPX+scratch Dockerfile, and CI pipeline with test/lint gates before deploy.
This commit is contained in:
57
db/db.go
57
db/db.go
@@ -1,33 +1,70 @@
|
||||
// Package db handles SQLite database setup, pragma configuration, and
|
||||
// goose migrations.
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrations embed.FS
|
||||
var MigrationFS embed.FS
|
||||
|
||||
var DB *sql.DB
|
||||
|
||||
func Init(dbPath string) error {
|
||||
func Init(dbPath string) (func(), error) {
|
||||
if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
|
||||
return nil, fmt.Errorf("creating data dir: %w", err)
|
||||
}
|
||||
|
||||
// busy_timeout must be first because the connection needs to block on
|
||||
// busy before WAL mode is set in case it hasn't been set already.
|
||||
pragmas := "?_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=journal_size_limit(200000000)&_pragma=synchronous(NORMAL)&_pragma=foreign_keys(ON)&_pragma=temp_store(MEMORY)&_pragma=cache_size(-32000)"
|
||||
var err error
|
||||
DB, err = sql.Open("sqlite", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
|
||||
DB, err = goose.OpenDBWithDriver("sqlite", dbPath+pragmas)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, fmt.Errorf("opening database: %w", err)
|
||||
}
|
||||
DB.SetMaxOpenConns(1)
|
||||
|
||||
goose.SetBaseFS(migrations)
|
||||
if err := DB.Ping(); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("pinging database: %w", err), DB.Close())
|
||||
}
|
||||
slog.Info("db connected", "db", dbPath)
|
||||
|
||||
sub, err := fs.Sub(MigrationFS, "migrations")
|
||||
if err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("migrations sub fs: %w", err), DB.Close())
|
||||
}
|
||||
goose.SetBaseFS(sub)
|
||||
|
||||
if err := goose.SetDialect("sqlite3"); err != nil {
|
||||
return err
|
||||
return nil, errors.Join(fmt.Errorf("setting goose dialect: %w", err), DB.Close())
|
||||
}
|
||||
if err := goose.Up(DB, "migrations"); err != nil {
|
||||
return err
|
||||
if err := goose.Up(DB, "."); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("running migrations: %w", err), DB.Close())
|
||||
}
|
||||
|
||||
return nil
|
||||
if _, err := DB.Exec("PRAGMA optimize"); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("pragma optimize: %w", err), DB.Close())
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
if _, err := DB.Exec("PRAGMA optimize(0x10002)"); err != nil {
|
||||
slog.Error("pragma optimize at shutdown", "error", err)
|
||||
}
|
||||
if err := DB.Close(); err != nil {
|
||||
slog.Error("closing database", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
return cleanup, nil
|
||||
}
|
||||
|
||||
@@ -5,17 +5,17 @@ import (
|
||||
"database/sql"
|
||||
"slices"
|
||||
|
||||
"github.com/ryanhamamura/c4/db/gen"
|
||||
"github.com/ryanhamamura/c4/db/repository"
|
||||
"github.com/ryanhamamura/c4/game"
|
||||
"github.com/ryanhamamura/c4/snake"
|
||||
"github.com/ryanhamamura/c4/ui"
|
||||
)
|
||||
|
||||
type GamePersister struct {
|
||||
queries *gen.Queries
|
||||
queries *repository.Queries
|
||||
}
|
||||
|
||||
func NewGamePersister(q *gen.Queries) *GamePersister {
|
||||
func NewGamePersister(q *repository.Queries) *GamePersister {
|
||||
return &GamePersister{queries: q}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (p *GamePersister) SaveGame(g *game.Game) error {
|
||||
|
||||
_, err := p.queries.GetGame(ctx, g.ID)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = p.queries.CreateGame(ctx, gen.CreateGameParams{
|
||||
_, err = p.queries.CreateGame(ctx, repository.CreateGameParams{
|
||||
ID: g.ID,
|
||||
Board: g.BoardToJSON(),
|
||||
CurrentTurn: int64(g.CurrentTurn),
|
||||
@@ -51,7 +51,7 @@ func (p *GamePersister) SaveGame(g *game.Game) error {
|
||||
rematchGameID = sql.NullString{String: *g.RematchGameID, Valid: true}
|
||||
}
|
||||
|
||||
return p.queries.UpdateGame(ctx, gen.UpdateGameParams{
|
||||
return p.queries.UpdateGame(ctx, repository.UpdateGameParams{
|
||||
Board: g.BoardToJSON(),
|
||||
CurrentTurn: int64(g.CurrentTurn),
|
||||
Status: int64(g.Status),
|
||||
@@ -100,7 +100,7 @@ func (p *GamePersister) SaveGamePlayer(gameID string, player *game.Player, slot
|
||||
guestPlayerID = sql.NullString{String: string(player.ID), Valid: true}
|
||||
}
|
||||
|
||||
return p.queries.CreateGamePlayer(ctx, gen.CreateGamePlayerParams{
|
||||
return p.queries.CreateGamePlayer(ctx, repository.CreateGamePlayerParams{
|
||||
GameID: gameID,
|
||||
UserID: userID,
|
||||
GuestPlayerID: guestPlayerID,
|
||||
@@ -144,10 +144,10 @@ func (p *GamePersister) DeleteGame(id string) error {
|
||||
|
||||
// SnakePersister implements snake.Persister
|
||||
type SnakePersister struct {
|
||||
queries *gen.Queries
|
||||
queries *repository.Queries
|
||||
}
|
||||
|
||||
func NewSnakePersister(q *gen.Queries) *SnakePersister {
|
||||
func NewSnakePersister(q *repository.Queries) *SnakePersister {
|
||||
return &SnakePersister{queries: q}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (p *SnakePersister) SaveSnakeGame(sg *snake.SnakeGame) error {
|
||||
|
||||
_, err := p.queries.GetSnakeGame(ctx, sg.ID)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = p.queries.CreateSnakeGame(ctx, gen.CreateSnakeGameParams{
|
||||
_, err = p.queries.CreateSnakeGame(ctx, repository.CreateSnakeGameParams{
|
||||
ID: sg.ID,
|
||||
Board: boardJSON,
|
||||
Status: int64(sg.Status),
|
||||
@@ -192,7 +192,7 @@ func (p *SnakePersister) SaveSnakeGame(sg *snake.SnakeGame) error {
|
||||
rematchGameID = sql.NullString{String: *sg.RematchGameID, Valid: true}
|
||||
}
|
||||
|
||||
return p.queries.UpdateSnakeGame(ctx, gen.UpdateSnakeGameParams{
|
||||
return p.queries.UpdateSnakeGame(ctx, repository.UpdateSnakeGameParams{
|
||||
Board: boardJSON,
|
||||
Status: int64(sg.Status),
|
||||
WinnerUserID: winnerUserID,
|
||||
@@ -247,7 +247,7 @@ func (p *SnakePersister) SaveSnakePlayer(gameID string, player *snake.Player) er
|
||||
guestPlayerID = sql.NullString{String: string(player.ID), Valid: true}
|
||||
}
|
||||
|
||||
return p.queries.CreateSnakePlayer(ctx, gen.CreateSnakePlayerParams{
|
||||
return p.queries.CreateSnakePlayer(ctx, repository.CreateSnakePlayerParams{
|
||||
GameID: gameID,
|
||||
UserID: userID,
|
||||
GuestPlayerID: guestPlayerID,
|
||||
@@ -290,15 +290,15 @@ func (p *SnakePersister) DeleteSnakeGame(id string) error {
|
||||
}
|
||||
|
||||
type ChatPersister struct {
|
||||
queries *gen.Queries
|
||||
queries *repository.Queries
|
||||
}
|
||||
|
||||
func NewChatPersister(q *gen.Queries) *ChatPersister {
|
||||
func NewChatPersister(q *repository.Queries) *ChatPersister {
|
||||
return &ChatPersister{queries: q}
|
||||
}
|
||||
|
||||
func (p *ChatPersister) SaveChatMessage(gameID string, msg ui.C4ChatMessage) error {
|
||||
return p.queries.CreateChatMessage(context.Background(), gen.CreateChatMessageParams{
|
||||
return p.queries.CreateChatMessage(context.Background(), repository.CreateChatMessageParams{
|
||||
GameID: gameID,
|
||||
Nickname: msg.Nickname,
|
||||
Color: int64(msg.Color),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// sqlc v1.30.0
|
||||
// source: chat.sql
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -2,7 +2,7 @@
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -3,7 +3,7 @@
|
||||
// sqlc v1.30.0
|
||||
// source: games.sql
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -2,7 +2,7 @@
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
@@ -3,7 +3,7 @@
|
||||
// sqlc v1.30.0
|
||||
// source: snake_games.sql
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -3,7 +3,7 @@
|
||||
// sqlc v1.30.0
|
||||
// source: users.sql
|
||||
|
||||
package gen
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -5,5 +5,9 @@ sql:
|
||||
schema: "migrations"
|
||||
gen:
|
||||
go:
|
||||
package: "gen"
|
||||
out: "gen"
|
||||
package: "repository"
|
||||
out: "repository"
|
||||
emit_db_tags: true
|
||||
emit_json_tags: true
|
||||
emit_result_struct_pointers: true
|
||||
emit_pointers_for_null_types: true
|
||||
|
||||
Reference in New Issue
Block a user