mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
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
This commit is contained in:
149
common/db/db.go
Normal file
149
common/db/db.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// Package db provides a transparent database wrapper that rewrites
|
||||
// PostgreSQL-style SQL for SQLite when needed.
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// DB wraps *sqlx.DB and transparently adapts PostgreSQL queries for SQLite.
|
||||
// For PostgreSQL, all methods are simple pass-throughs with zero overhead.
|
||||
type DB struct {
|
||||
*sqlx.DB
|
||||
sqlite bool
|
||||
}
|
||||
|
||||
// Wrap creates a DB wrapper around an existing *sqlx.DB connection.
|
||||
// Returns nil if db is nil.
|
||||
func Wrap(db *sqlx.DB) *DB {
|
||||
if db == nil {
|
||||
return nil
|
||||
}
|
||||
return &DB{
|
||||
DB: db,
|
||||
sqlite: IsSQLite(db),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DB) adapt(query string) string {
|
||||
if !d.sqlite {
|
||||
return query
|
||||
}
|
||||
return AdaptSQL(query)
|
||||
}
|
||||
|
||||
// Exec executes a query without returning any rows.
|
||||
func (d *DB) Exec(query string, args ...interface{}) (sql.Result, error) {
|
||||
return d.DB.Exec(d.adapt(query), args...)
|
||||
}
|
||||
|
||||
// ExecContext executes a query without returning any rows.
|
||||
func (d *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
|
||||
return d.DB.ExecContext(ctx, d.adapt(query), args...)
|
||||
}
|
||||
|
||||
// Query executes a query that returns rows.
|
||||
func (d *DB) Query(query string, args ...interface{}) (*sql.Rows, error) {
|
||||
return d.DB.Query(d.adapt(query), args...)
|
||||
}
|
||||
|
||||
// QueryContext executes a query that returns rows.
|
||||
func (d *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
|
||||
return d.DB.QueryContext(ctx, d.adapt(query), args...)
|
||||
}
|
||||
|
||||
// QueryRow executes a query that is expected to return at most one row.
|
||||
func (d *DB) QueryRow(query string, args ...interface{}) *sql.Row {
|
||||
return d.DB.QueryRow(d.adapt(query), args...)
|
||||
}
|
||||
|
||||
// QueryRowContext executes a query that is expected to return at most one row.
|
||||
func (d *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
|
||||
return d.DB.QueryRowContext(ctx, d.adapt(query), args...)
|
||||
}
|
||||
|
||||
// Get queries a single row and scans it into dest.
|
||||
func (d *DB) Get(dest interface{}, query string, args ...interface{}) error {
|
||||
return d.DB.Get(dest, d.adapt(query), args...)
|
||||
}
|
||||
|
||||
// Select queries multiple rows and scans them into dest.
|
||||
func (d *DB) Select(dest interface{}, query string, args ...interface{}) error {
|
||||
return d.DB.Select(dest, d.adapt(query), args...)
|
||||
}
|
||||
|
||||
// Queryx executes a query that returns sqlx.Rows.
|
||||
func (d *DB) Queryx(query string, args ...interface{}) (*sqlx.Rows, error) {
|
||||
return d.DB.Queryx(d.adapt(query), args...)
|
||||
}
|
||||
|
||||
// QueryRowx executes a query that returns a sqlx.Row.
|
||||
func (d *DB) QueryRowx(query string, args ...interface{}) *sqlx.Row {
|
||||
return d.DB.QueryRowx(d.adapt(query), args...)
|
||||
}
|
||||
|
||||
// QueryRowxContext executes a query that returns a sqlx.Row.
|
||||
func (d *DB) QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row {
|
||||
return d.DB.QueryRowxContext(ctx, d.adapt(query), args...)
|
||||
}
|
||||
|
||||
// BeginTxx starts a new transaction with context and options.
|
||||
// The returned Tx wrapper adapts queries the same way as DB.
|
||||
func (d *DB) BeginTxx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) {
|
||||
tx, err := d.DB.BeginTxx(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Tx{Tx: tx, sqlite: d.sqlite}, nil
|
||||
}
|
||||
|
||||
// Tx wraps *sqlx.Tx and transparently adapts PostgreSQL queries for SQLite.
|
||||
type Tx struct {
|
||||
*sqlx.Tx
|
||||
sqlite bool
|
||||
}
|
||||
|
||||
func (t *Tx) adapt(query string) string {
|
||||
if !t.sqlite {
|
||||
return query
|
||||
}
|
||||
return AdaptSQL(query)
|
||||
}
|
||||
|
||||
// Exec executes a query without returning any rows.
|
||||
func (t *Tx) Exec(query string, args ...interface{}) (sql.Result, error) {
|
||||
return t.Tx.Exec(t.adapt(query), args...)
|
||||
}
|
||||
|
||||
// ExecContext executes a query without returning any rows.
|
||||
func (t *Tx) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
|
||||
return t.Tx.ExecContext(ctx, t.adapt(query), args...)
|
||||
}
|
||||
|
||||
// Query executes a query that returns rows.
|
||||
func (t *Tx) Query(query string, args ...interface{}) (*sql.Rows, error) {
|
||||
return t.Tx.Query(t.adapt(query), args...)
|
||||
}
|
||||
|
||||
// QueryRow executes a query that is expected to return at most one row.
|
||||
func (t *Tx) QueryRow(query string, args ...interface{}) *sql.Row {
|
||||
return t.Tx.QueryRow(t.adapt(query), args...)
|
||||
}
|
||||
|
||||
// Queryx executes a query that returns sqlx.Rows.
|
||||
func (t *Tx) Queryx(query string, args ...interface{}) (*sqlx.Rows, error) {
|
||||
return t.Tx.Queryx(t.adapt(query), args...)
|
||||
}
|
||||
|
||||
// QueryRowx executes a query that returns a sqlx.Row.
|
||||
func (t *Tx) QueryRowx(query string, args ...interface{}) *sqlx.Row {
|
||||
return t.Tx.QueryRowx(t.adapt(query), args...)
|
||||
}
|
||||
|
||||
// Get queries a single row and scans it into dest.
|
||||
func (t *Tx) Get(dest interface{}, query string, args ...interface{}) error {
|
||||
return t.Tx.Get(dest, t.adapt(query), args...)
|
||||
}
|
||||
Reference in New Issue
Block a user