Replace Postgres with SQLite
Ten members and a handful of songs a week never needed a database server,
and the server was the last thing making this a two-container deployment.
modernc.org/sqlite is pure Go, so CGO_ENABLED=0 survives and the dependency
count is unchanged: pgx out, sqlite in.
The port stayed small because the driver matches $1-style placeholders
against argument ordinals exactly as pgx does, so no query needed rewriting
for parameters. What did change:
- timestamptz becomes timestamp holding UTC 'YYYY-MM-DD HH:MM:SS'. The
declared type is what makes the driver return time.Time, and the
fixed-width UTC string is what makes ordering and comparison against
datetime('now') mean what they say.
- interval has no equivalent: sessions.idle_ttl is seconds, and the review
edit window travels as a SQLite date modifier string.
- No stddev_pop, so the divisive and unified boards spell the population
formula out, guarded with max(0.0, ...) because cancellation returns a
tiny negative when every score is identical.
- foreign_keys is off by default, so the cascades only exist because the
pragma is set on every connection.
Drops the postgres service, its healthcheck, the depends_on gate, the
startup retry loop and POSTGRES_PASSWORD. ./storage is now the whole
backup. Tests get a fresh database file per test and run everywhere
instead of skipping without TEST_DATABASE_URL.
This commit is contained in:
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -12,8 +13,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -147,12 +146,12 @@ func (a *app) submitError(w http.ResponseWriter, r *http.Request, status int, ms
|
||||
// from `submissions` is why this also looks at `songs`.
|
||||
func (a *app) overQuota(ctx context.Context, userID int64) (bool, error) {
|
||||
var n int
|
||||
err := a.pool.QueryRow(ctx, `
|
||||
err := a.db.QueryRowContext(ctx, `
|
||||
select (select count(*) from submissions
|
||||
where user_id = $1 and status <> 'failed'
|
||||
and created_at > now() - interval '24 hours')
|
||||
and created_at > datetime('now', '-24 hours'))
|
||||
+ (select count(*) from songs
|
||||
where submitted_by = $1 and created_at > now() - interval '24 hours')`,
|
||||
where submitted_by = $1 and created_at > datetime('now', '-24 hours'))`,
|
||||
userID).Scan(&n)
|
||||
return n >= maxPerDay, err
|
||||
}
|
||||
@@ -196,7 +195,7 @@ func (a *app) submit(w http.ResponseWriter, r *http.Request) {
|
||||
defer file.Close()
|
||||
|
||||
var subID int64
|
||||
err = a.pool.QueryRow(r.Context(),
|
||||
err = a.db.QueryRowContext(r.Context(),
|
||||
`insert into submissions (user_id, status) values ($1, 'queued') returning id`,
|
||||
m.ID).Scan(&subID)
|
||||
if err != nil {
|
||||
@@ -236,7 +235,7 @@ func (a *app) submit(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := a.pool.Exec(r.Context(),
|
||||
if _, err := a.db.ExecContext(r.Context(),
|
||||
`update submissions set tmp_path = $2, title = nullif($3, ''), artist = nullif($4, '')
|
||||
where id = $1`, subID, src, meta.Title, meta.Artist); err != nil {
|
||||
slog.Error("save metadata", "ctx", "submissions", "error", err, "submission", subID)
|
||||
@@ -268,7 +267,7 @@ func (a *app) submitURL(w http.ResponseWriter, r *http.Request, userID int64, ra
|
||||
}
|
||||
|
||||
var subID int64
|
||||
err = a.pool.QueryRow(r.Context(), `
|
||||
err = a.db.QueryRowContext(r.Context(), `
|
||||
insert into submissions (user_id, status, source_url, title, artist)
|
||||
values ($1, 'queued', $2, nullif($3, ''), nullif($4, '')) returning id`,
|
||||
userID, link, meta.Title, meta.Artist).Scan(&subID)
|
||||
@@ -294,7 +293,7 @@ func (a *app) retry(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "ei uudelleenyritettävissä", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if _, err := a.pool.Exec(r.Context(),
|
||||
if _, err := a.db.ExecContext(r.Context(),
|
||||
`update submissions set status = 'queued', status_msg = null where id = $1`, s.ID); err != nil {
|
||||
slog.Error("retry", "ctx", "submissions", "error", err, "submission", s.ID)
|
||||
http.Error(w, "virhe", http.StatusInternalServerError)
|
||||
@@ -309,7 +308,7 @@ func (a *app) discardSubmission(ctx context.Context, subID int64, path string) {
|
||||
if path != "" {
|
||||
os.Remove(path)
|
||||
}
|
||||
if _, err := a.pool.Exec(ctx, `delete from submissions where id = $1`, subID); err != nil {
|
||||
if _, err := a.db.ExecContext(ctx, `delete from submissions where id = $1`, subID); err != nil {
|
||||
slog.Error("discard submission", "ctx", "submissions", "error", err, "submission", subID)
|
||||
}
|
||||
}
|
||||
@@ -348,7 +347,7 @@ func (a *app) process(subID int64, sourceURL, src string) {
|
||||
a.setStatus(ctx, subID, "failed", "lataus ei tuottanut tiedostoa")
|
||||
return
|
||||
}
|
||||
if _, err := a.pool.Exec(ctx,
|
||||
if _, err := a.db.ExecContext(ctx,
|
||||
`update submissions set tmp_path = $2 where id = $1`, subID, src); err != nil {
|
||||
slog.Error("save tmp path", "ctx", "submissions", "error", err, "submission", subID)
|
||||
}
|
||||
@@ -370,7 +369,7 @@ func (a *app) process(subID int64, sourceURL, src string) {
|
||||
// The original is discarded as soon as the Opus exists.
|
||||
os.Remove(src)
|
||||
|
||||
if _, err := a.pool.Exec(ctx,
|
||||
if _, err := a.db.ExecContext(ctx,
|
||||
`update submissions set status = 'ready', status_msg = null, tmp_path = $2 where id = $1`,
|
||||
subID, out); err != nil {
|
||||
slog.Error("mark ready", "ctx", "submissions", "error", err, "submission", subID)
|
||||
@@ -382,7 +381,7 @@ func (a *app) process(subID int64, sourceURL, src string) {
|
||||
// so it covers well-tagged music; the Hae sanoitukset button on the waiting page is what
|
||||
// covers everything else, once the submitter has fixed the title and artist.
|
||||
var title, artist string
|
||||
if err := a.pool.QueryRow(ctx,
|
||||
if err := a.db.QueryRowContext(ctx,
|
||||
`select coalesce(title, ''), coalesce(artist, '') from submissions where id = $1`,
|
||||
subID).Scan(&title, &artist); err != nil {
|
||||
return
|
||||
@@ -395,7 +394,7 @@ func (a *app) process(subID int64, sourceURL, src string) {
|
||||
}
|
||||
|
||||
func (a *app) setStatus(ctx context.Context, subID int64, status, msg string) {
|
||||
if _, err := a.pool.Exec(ctx,
|
||||
if _, err := a.db.ExecContext(ctx,
|
||||
`update submissions set status = $2, status_msg = nullif($3, '') where id = $1`,
|
||||
subID, status, msg); err != nil {
|
||||
slog.Error("set status", "ctx", "submissions", "error", err, "submission", subID)
|
||||
@@ -412,14 +411,14 @@ func (a *app) loadSubmission(w http.ResponseWriter, r *http.Request) *submission
|
||||
return nil
|
||||
}
|
||||
var s submission
|
||||
err = a.pool.QueryRow(r.Context(), `
|
||||
err = a.db.QueryRowContext(r.Context(), `
|
||||
select id, user_id, status, status_msg, source_url, coalesce(tmp_path, ''),
|
||||
coalesce(title, ''), coalesce(artist, ''), coalesce(genre, ''),
|
||||
coalesce(description, ''), coalesce(lyrics, ''), created_at
|
||||
from submissions where id = $1`, id).
|
||||
Scan(&s.ID, &s.UserID, &s.Status, &s.StatusMsg, &s.SourceURL, &s.TmpPath,
|
||||
&s.Title, &s.Artist, &s.Genre, &s.Description, &s.Lyrics, &s.CreatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
http.NotFound(w, r)
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@@ -473,7 +472,7 @@ func (a *app) saveMetadata(ctx context.Context, subID int64, r *http.Request) er
|
||||
// worker had just fetched.
|
||||
has := func(field string) bool { _, ok := r.Form[field]; return ok }
|
||||
|
||||
_, err := a.pool.Exec(ctx, `
|
||||
_, err := a.db.ExecContext(ctx, `
|
||||
update submissions set
|
||||
title = case when $2 then nullif($3, '') else title end,
|
||||
artist = case when $4 then nullif($5, '') else artist end,
|
||||
@@ -557,16 +556,16 @@ func (a *app) publish(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := a.pool.Begin(r.Context())
|
||||
tx, err := a.db.BeginTx(r.Context(), nil)
|
||||
if err != nil {
|
||||
slog.Error("begin publish", "ctx", "submissions", "error", err)
|
||||
http.Error(w, "virhe", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(r.Context())
|
||||
defer tx.Rollback()
|
||||
|
||||
var songID int64
|
||||
err = tx.QueryRow(r.Context(), `
|
||||
err = tx.QueryRowContext(r.Context(), `
|
||||
insert into songs (title, artist, genre, description, lyrics, audio_file, duration_seconds,
|
||||
source_url, submitted_by)
|
||||
values ($1, $2, $3, $4, $5, '', $6, $7, $8) returning id`,
|
||||
@@ -587,7 +586,7 @@ func (a *app) publish(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "virhe", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(),
|
||||
if _, err := tx.ExecContext(r.Context(),
|
||||
`update songs set audio_file = $2 where id = $1`,
|
||||
songID, filepath.Base(dst)); err != nil {
|
||||
os.Rename(dst, src)
|
||||
@@ -595,13 +594,13 @@ func (a *app) publish(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "virhe", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if _, err := tx.Exec(r.Context(), `delete from submissions where id = $1`, s.ID); err != nil {
|
||||
if _, err := tx.ExecContext(r.Context(), `delete from submissions where id = $1`, s.ID); err != nil {
|
||||
os.Rename(dst, src)
|
||||
slog.Error("delete submission", "ctx", "submissions", "error", err, "submission", s.ID)
|
||||
http.Error(w, "virhe", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := tx.Commit(r.Context()); err != nil {
|
||||
if err := tx.Commit(); err != nil {
|
||||
os.Rename(dst, src)
|
||||
slog.Error("commit publish", "ctx", "songs", "error", err, "submission", s.ID)
|
||||
http.Error(w, "virhe", http.StatusInternalServerError)
|
||||
@@ -635,7 +634,7 @@ func nilIfEmpty(s string) *string {
|
||||
|
||||
// Own in-flight submissions, for the home page — otherwise a submission is only reachable by URL.
|
||||
func (a *app) mySubmissions(ctx context.Context, userID int64) ([]*submission, error) {
|
||||
rows, err := a.pool.Query(ctx, `
|
||||
rows, err := a.db.QueryContext(ctx, `
|
||||
select id, user_id, status, status_msg, source_url, coalesce(tmp_path, ''),
|
||||
coalesce(title, ''), coalesce(artist, ''), coalesce(genre, ''),
|
||||
coalesce(description, ''), created_at
|
||||
|
||||
Reference in New Issue
Block a user