Vellum/internal/core/db.go
mithral eb1ad6d920 Phase 0: skeleton, config, migrations, health check, container
Bootstraps Vellum as a single Go binary with one SQLite database.

- core: env-driven config, slog logging, SQLite (WAL) with pooled pragmas,
  embedded migration runner keyed on PRAGMA user_version
- migrations/0001_core.sql: identity, media, user state, infra. Media items
  are global; all per-user state (progress, favorites, grants) is a join.
  One polymorphic progress table covers every media kind.
- httpapi: request-ID, structured request logging, panic recovery, JSON
  endpoints for /healthz and /api/v1/version
- multi-arch Dockerfile (pure-Go SQLite, CGO_ENABLED=0) with an entrypoint
  that honours PUID/PGID like the rest of the self-hosted stack
- tests for migration idempotency, FK enforcement, WAL, config, middleware

Media does not exist yet. Nothing is exposed beyond health and version.
2026-10-09 16:26:08 -04:00

60 lines
1.8 KiB
Go

package core
import (
"database/sql"
"fmt"
"net/url"
"strings"
"time"
// Pure-Go SQLite driver. No cgo means CGO_ENABLED=0 static binaries and
// trivial cross-compilation for arm64.
_ "modernc.org/sqlite"
)
// pragmas applied to every pooled connection, via the DSN.
//
// These are passed in the DSN rather than run as statements after connecting
// because database/sql pools connections: a one-off PRAGMA would configure
// only whichever single connection happened to serve it.
var sqlitePragmas = []string{
"journal_mode(WAL)", // concurrent readers during a scan
"busy_timeout(5000)", // wait for the writer instead of failing
"foreign_keys(1)", // enforce the schema's referential integrity
"synchronous(NORMAL)", // safe under WAL, far faster than FULL
"cache_size(-32000)", // ~32 MB page cache
}
// OpenDB opens the SQLite database with the pragmas Vellum depends on.
func OpenDB(path string) (*sql.DB, error) {
db, err := sql.Open("sqlite", buildDSN(path, sqlitePragmas))
if err != nil {
return nil, fmt.Errorf("open sqlite %s: %w", path, err)
}
// SQLite is a single-writer database. A large pool just produces
// SQLITE_BUSY contention; a small pool with a busy_timeout is faster.
db.SetMaxOpenConns(8)
db.SetMaxIdleConns(8)
db.SetConnMaxLifetime(0)
db.SetConnMaxIdleTime(5 * time.Minute)
if err := db.Ping(); err != nil {
_ = db.Close()
return nil, fmt.Errorf("ping sqlite %s: %w", path, err)
}
return db, nil
}
// buildDSN assembles a modernc.org/sqlite DSN with repeated _pragma params.
func buildDSN(path string, pragmas []string) string {
var b strings.Builder
b.WriteString("file:")
b.WriteString(path)
b.WriteString("?_time_format=sqlite")
for _, p := range pragmas {
b.WriteString("&_pragma=")
b.WriteString(url.QueryEscape(p))
}
return b.String()
}