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:
+19
-27
@@ -6,34 +6,26 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Set TEST_DATABASE_URL to a throwaway database — these drop and recreate the public schema.
|
||||
// A fresh database file per test, thrown away with the temp dir. No server to point at, so these
|
||||
// run everywhere rather than only where someone remembered to set an env var.
|
||||
func testApp(t *testing.T) *app {
|
||||
t.Helper()
|
||||
dbURL := os.Getenv("TEST_DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
t.Skip("TEST_DATABASE_URL not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, dbURL)
|
||||
db, err := openDB(filepath.Join(t.TempDir(), "test.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(pool.Close)
|
||||
if _, err := pool.Exec(ctx, `drop schema public cascade; create schema public`); err != nil {
|
||||
t.Cleanup(func() { db.Close() })
|
||||
if err := migrate(ctx, db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := migrate(ctx, pool); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &app{cfg: config{adminUser: "admin", adminPass: "s3cret"}, pool: pool}
|
||||
return &app{cfg: config{adminUser: "admin", adminPass: "s3cret"}, db: db}
|
||||
}
|
||||
|
||||
func post(t *testing.T, h http.Handler, path string, form url.Values) *httptest.ResponseRecorder {
|
||||
@@ -48,7 +40,7 @@ func post(t *testing.T, h http.Handler, path string, form url.Values) *httptest.
|
||||
func (a *app) inviteValid(t *testing.T, code string) bool {
|
||||
t.Helper()
|
||||
var valid bool
|
||||
if err := a.pool.QueryRow(context.Background(),
|
||||
if err := a.db.QueryRowContext(context.Background(),
|
||||
`select is_valid from invites where code = $1`, code).Scan(&valid); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -61,10 +53,10 @@ func TestInviteIsSpentOnlyBySuccess(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mux := a.withMember(a.memberMux())
|
||||
|
||||
if _, err := a.pool.Exec(ctx, `insert into invites (code) values ('kutsu1')`); err != nil {
|
||||
if _, err := a.db.ExecContext(ctx, `insert into invites (code) values ('kutsu1')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := a.pool.Exec(ctx,
|
||||
if _, err := a.db.ExecContext(ctx,
|
||||
`insert into users (name, email, password_hash) values ('Esa', '[email protected]', 'x')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -131,7 +123,7 @@ func TestLoginHandlerRefusesAfterTooManyFailures(t *testing.T) {
|
||||
func (a *app) seedMember(t *testing.T, email string) int64 {
|
||||
t.Helper()
|
||||
var id int64
|
||||
err := a.pool.QueryRow(context.Background(),
|
||||
err := a.db.QueryRowContext(context.Background(),
|
||||
`insert into users (name, email, password_hash) values ('Esa', $1, 'x') returning id`,
|
||||
email).Scan(&id)
|
||||
if err != nil {
|
||||
@@ -161,8 +153,8 @@ func TestSessionIdleTimeout(t *testing.T) {
|
||||
}
|
||||
|
||||
// Age it past the idle window: the timeout is what expiry means, so this is the whole rule.
|
||||
if _, err := a.pool.Exec(ctx,
|
||||
`update sessions set expires_at = now() - interval '1 second' where token = $1`, live); err != nil {
|
||||
if _, err := a.db.ExecContext(ctx,
|
||||
`update sessions set expires_at = datetime('now', '-1 second') where token = $1`, live); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m := a.sessionFor(t, live); m != nil {
|
||||
@@ -174,15 +166,15 @@ func TestSessionIdleTimeout(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := a.pool.Exec(ctx,
|
||||
`update sessions set expires_at = now() + interval '1 hour' where token = $1`, fresh); err != nil {
|
||||
if _, err := a.db.ExecContext(ctx,
|
||||
`update sessions set expires_at = datetime('now', '+1 hour') where token = $1`, fresh); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m := a.sessionFor(t, fresh); m == nil {
|
||||
t.Fatal("session inside the window did not resolve")
|
||||
}
|
||||
var expires time.Time
|
||||
if err := a.pool.QueryRow(ctx,
|
||||
if err := a.db.QueryRowContext(ctx,
|
||||
`select expires_at from sessions where token = $1`, fresh).Scan(&expires); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -196,7 +188,7 @@ func TestBanDropsSessionsAndBlocksLogin(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mux := a.withMember(a.memberMux())
|
||||
|
||||
if _, err := a.pool.Exec(ctx, `insert into invites (code) values ('kutsu2')`); err != nil {
|
||||
if _, err := a.db.ExecContext(ctx, `insert into invites (code) values ('kutsu2')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := post(t, mux, "/register", url.Values{
|
||||
@@ -206,7 +198,7 @@ func TestBanDropsSessionsAndBlocksLogin(t *testing.T) {
|
||||
t.Fatalf("registration: status = %d, want 303", w.Code)
|
||||
}
|
||||
var id int64
|
||||
if err := a.pool.QueryRow(ctx, `select id from users where email = '[email protected]'`).Scan(&id); err != nil {
|
||||
if err := a.db.QueryRowContext(ctx, `select id from users where email = '[email protected]'`).Scan(&id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -216,7 +208,7 @@ func TestBanDropsSessionsAndBlocksLogin(t *testing.T) {
|
||||
}
|
||||
|
||||
var sessions int
|
||||
if err := a.pool.QueryRow(ctx,
|
||||
if err := a.db.QueryRowContext(ctx,
|
||||
`select count(*) from sessions where user_id = $1`, id).Scan(&sessions); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user