feat: persist chat messages to SQLite (#1)
All checks were successful
Deploy c4 / deploy (push) Successful in 44s

This commit was merged in pull request #1.
This commit is contained in:
2026-02-13 22:00:01 +00:00
parent 08c20a1732
commit 3593197271
6 changed files with 150 additions and 4 deletions

71
db/gen/chat.sql.go Normal file
View File

@@ -0,0 +1,71 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: chat.sql
package gen
import (
"context"
)
const createChatMessage = `-- name: CreateChatMessage :exec
INSERT INTO chat_messages (game_id, nickname, color, message, created_at)
VALUES (?, ?, ?, ?, ?)
`
type CreateChatMessageParams struct {
GameID string
Nickname string
Color int64
Message string
CreatedAt int64
}
func (q *Queries) CreateChatMessage(ctx context.Context, arg CreateChatMessageParams) error {
_, err := q.db.ExecContext(ctx, createChatMessage,
arg.GameID,
arg.Nickname,
arg.Color,
arg.Message,
arg.CreatedAt,
)
return err
}
const getChatMessages = `-- name: GetChatMessages :many
SELECT id, game_id, nickname, color, message, created_at FROM chat_messages
WHERE game_id = ?
ORDER BY created_at DESC, id DESC
LIMIT 50
`
func (q *Queries) GetChatMessages(ctx context.Context, gameID string) ([]ChatMessage, error) {
rows, err := q.db.QueryContext(ctx, getChatMessages, gameID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ChatMessage
for rows.Next() {
var i ChatMessage
if err := rows.Scan(
&i.ID,
&i.GameID,
&i.Nickname,
&i.Color,
&i.Message,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

View File

@@ -8,6 +8,15 @@ import (
"database/sql"
)
type ChatMessage struct {
ID int64
GameID string
Nickname string
Color int64
Message string
CreatedAt int64
}
type Game struct {
ID string
Board string

View File

@@ -0,0 +1,15 @@
-- +goose Up
CREATE TABLE chat_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
game_id TEXT NOT NULL,
nickname TEXT NOT NULL,
color INTEGER NOT NULL,
message TEXT NOT NULL,
created_at INTEGER NOT NULL,
FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE
);
CREATE INDEX idx_chat_messages_game ON chat_messages(game_id, created_at);
-- +goose Down
DROP INDEX IF EXISTS idx_chat_messages_game;
DROP TABLE IF EXISTS chat_messages;

View File

@@ -3,10 +3,12 @@ package db
import (
"context"
"database/sql"
"slices"
"github.com/ryanhamamura/c4/db/gen"
"github.com/ryanhamamura/c4/game"
"github.com/ryanhamamura/c4/snake"
"github.com/ryanhamamura/c4/ui"
)
type GamePersister struct {
@@ -286,3 +288,40 @@ func (p *SnakePersister) DeleteSnakeGame(id string) error {
ctx := context.Background()
return p.queries.DeleteSnakeGame(ctx, id)
}
type ChatPersister struct {
queries *gen.Queries
}
func NewChatPersister(q *gen.Queries) *ChatPersister {
return &ChatPersister{queries: q}
}
func (p *ChatPersister) SaveChatMessage(gameID string, msg ui.C4ChatMessage) error {
return p.queries.CreateChatMessage(context.Background(), gen.CreateChatMessageParams{
GameID: gameID,
Nickname: msg.Nickname,
Color: int64(msg.Color),
Message: msg.Message,
CreatedAt: msg.Time,
})
}
func (p *ChatPersister) LoadChatMessages(gameID string) ([]ui.C4ChatMessage, error) {
rows, err := p.queries.GetChatMessages(context.Background(), gameID)
if err != nil {
return nil, err
}
msgs := make([]ui.C4ChatMessage, len(rows))
for i, r := range rows {
msgs[i] = ui.C4ChatMessage{
Nickname: r.Nickname,
Color: int(r.Color),
Message: r.Message,
Time: r.CreatedAt,
}
}
// Query returns newest-first; reverse to oldest-first for display
slices.Reverse(msgs)
return msgs, nil
}

9
db/queries/chat.sql Normal file
View File

@@ -0,0 +1,9 @@
-- name: CreateChatMessage :exec
INSERT INTO chat_messages (game_id, nickname, color, message, created_at)
VALUES (?, ?, ?, ?, ?);
-- name: GetChatMessages :many
SELECT * FROM chat_messages
WHERE game_id = ?
ORDER BY created_at DESC, id DESC
LIMIT 50;

11
main.go
View File

@@ -27,9 +27,10 @@ import (
)
var (
store = game.NewGameStore()
snakeStore = snake.NewSnakeStore()
queries *gen.Queries
store = game.NewGameStore()
snakeStore = snake.NewSnakeStore()
queries *gen.Queries
chatPersister *db.ChatPersister
)
//go:embed assets
@@ -61,6 +62,7 @@ func main() {
queries = gen.New(db.DB)
store.SetPersister(db.NewGamePersister(queries))
snakeStore.SetPersister(db.NewSnakePersister(queries))
chatPersister = db.NewChatPersister(queries)
sessionManager, err := via.NewSQLiteSessionManager(db.DB)
if err != nil {
@@ -356,7 +358,7 @@ func main() {
colSignal := c.Signal(0)
showGuestPrompt := c.Signal(false)
chatMsg := c.Signal("")
var chatMessages []ui.C4ChatMessage
chatMessages, _ := chatPersister.LoadChatMessages(gameID)
var chatMu sync.Mutex
goToLogin := c.Action(func() {
@@ -462,6 +464,7 @@ func main() {
Message: msg,
Time: time.Now().UnixMilli(),
}
chatPersister.SaveChatMessage(gameID, cm)
data, err := json.Marshal(cm)
if err != nil {
return