Files
Erupe/common/db/adapter.go
Houmgaor ecfe58ffb4 feat: add SQLite support, setup wizard enhancements, and live dashboard
Add zero-dependency SQLite mode so users can run Erupe without
PostgreSQL. A transparent db.DB wrapper auto-translates PostgreSQL
SQL ($N placeholders, now(), ::casts, ILIKE, public. prefix,
TRUNCATE) for SQLite at runtime — all 28 repo files use the wrapper
with no per-query changes needed.

Setup wizard gains two new steps: quest file detection with download
link, and gameplay presets (solo/small/community/rebalanced). The API
server gets a /dashboard endpoint with auto-refreshing stats.

CI release workflow now builds and pushes Docker images to GHCR
alongside binary artifacts on tag push.

Key changes:
- common/db: DB/Tx wrapper with 6 SQL translation rules
- server/migrations/sqlite: full SQLite schema (0001-0005)
- config: Database.Driver field ("postgres" or "sqlite")
- main.go: SQLite connection with WAL mode, single writer
- server/setup: quest check + preset selection steps
- server/api: /dashboard with live stats
- .github/workflows: Docker in release, deduplicate docker.yml
2026-03-05 18:00:30 +01:00

69 lines
2.4 KiB
Go

// Package db provides a database adapter that transparently translates
// PostgreSQL-style SQL to the active driver's dialect.
//
// When the driver is "sqlite", queries are rewritten on the fly:
// - $1, $2, ... → ?, ?, ...
// - now() → CURRENT_TIMESTAMP
// - ::type casts → removed
// - ILIKE → LIKE (SQLite LIKE is case-insensitive for ASCII)
package db
import (
"regexp"
"strings"
"github.com/jmoiron/sqlx"
)
// IsSQLite reports whether the given sqlx.DB is backed by a SQLite driver.
func IsSQLite(db *sqlx.DB) bool {
return db.DriverName() == "sqlite" || db.DriverName() == "sqlite3"
}
// Adapt rewrites a PostgreSQL query for the active driver.
// For Postgres it's a no-op. For SQLite it translates placeholders and
// Postgres-specific syntax.
func Adapt(db *sqlx.DB, query string) string {
if !IsSQLite(db) {
return query
}
return AdaptSQL(query)
}
// castRe matches Postgres type casts like ::int, ::text, ::timestamptz,
// ::character varying, etc.
// castRe matches Postgres type casts: ::int, ::text, ::timestamptz,
// ::character varying(N), etc. The space is allowed only when followed
// by a word char (e.g. "character varying") to avoid eating trailing spaces.
var castRe = regexp.MustCompile(`::[a-zA-Z_]\w*(?:\s+\w+)*(?:\([^)]*\))?`)
// dollarParamRe matches Postgres-style positional parameters: $1, $2, etc.
var dollarParamRe = regexp.MustCompile(`\$\d+`)
// AdaptSQL translates a PostgreSQL query to SQLite-compatible SQL.
// Exported so it can be tested without a real DB connection.
func AdaptSQL(query string) string {
// 1. Replace now() with CURRENT_TIMESTAMP
query = strings.ReplaceAll(query, "now()", "CURRENT_TIMESTAMP")
query = strings.ReplaceAll(query, "NOW()", "CURRENT_TIMESTAMP")
// 2. Strip Postgres type casts (::int, ::text, ::timestamptz, etc.)
query = castRe.ReplaceAllString(query, "")
// 3. ILIKE → LIKE (SQLite LIKE is case-insensitive for ASCII by default)
query = strings.ReplaceAll(query, " ILIKE ", " LIKE ")
query = strings.ReplaceAll(query, " ilike ", " LIKE ")
// 4. Strip "public." schema prefix (SQLite has no schemas)
query = strings.ReplaceAll(query, "public.", "")
// 5. TRUNCATE → DELETE FROM (SQLite has no TRUNCATE)
query = strings.ReplaceAll(query, "TRUNCATE ", "DELETE FROM ")
query = strings.ReplaceAll(query, "truncate ", "DELETE FROM ")
// 6. Replace $1,$2,... → ?,?,...
query = dollarParamRe.ReplaceAllString(query, "?")
return query
}