feat(db): add embedded auto-migrating schema system

Replace 4 independent schema management code paths (Docker shell
script, setup wizard pg_restore, test helpers, manual psql) with a
single migration runner embedded in the server binary.

The new server/migrations/ package uses Go embed to bundle all SQL
schemas. On startup, Migrate() creates a schema_version tracking
table, detects existing databases (auto-marks baseline as applied),
and runs pending migrations in transactions.

Key changes:
- Consolidated init.sql + 9.2-update + 33 patches into 0001_init.sql
- Setup wizard simplified to single "Apply schema" checkbox
- Test helpers use migrations.Migrate() instead of pg_restore
- Docker no longer needs schema volume mounts or init script
- Seed data (shops, events, gacha) embedded and applied via API
- Future migrations just add 0002_*.sql files — no manual steps
This commit is contained in:
Houmgaor
2026-02-23 21:19:21 +01:00
parent 6a7db47723
commit 27fb0faa1e
62 changed files with 4736 additions and 932 deletions

View File

@@ -6,10 +6,6 @@ import (
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
// clientModes returns all supported client version strings.
@@ -373,71 +369,3 @@ func createDatabase(host string, port int, user, password, dbName string) error
return nil
}
// applyInitSchema runs pg_restore to load the init.sql (PostgreSQL custom dump format).
func applyInitSchema(host string, port int, user, password, dbName string) error {
pgRestore, err := exec.LookPath("pg_restore")
if err != nil {
return fmt.Errorf("pg_restore not found in PATH: %w (install PostgreSQL client tools)", err)
}
schemaPath := filepath.Join("schemas", "init.sql")
if _, err := os.Stat(schemaPath); err != nil {
return fmt.Errorf("schema file not found: %s", schemaPath)
}
cmd := exec.Command(pgRestore,
"--host", host,
"--port", fmt.Sprint(port),
"--username", user,
"--dbname", dbName,
"--no-owner",
"--no-privileges",
schemaPath,
)
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", password))
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("pg_restore failed: %w\n%s", err, string(output))
}
return nil
}
// collectSQLFiles returns sorted .sql filenames from a directory.
func collectSQLFiles(dir string) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("reading directory %s: %w", dir, err)
}
var files []string
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
files = append(files, e.Name())
}
}
sort.Strings(files)
return files, nil
}
// applySQLFiles executes all .sql files in a directory in sorted order.
func applySQLFiles(db *sql.DB, dir string) ([]string, error) {
files, err := collectSQLFiles(dir)
if err != nil {
return nil, err
}
var applied []string
for _, f := range files {
path := filepath.Join(dir, f)
data, err := os.ReadFile(path)
if err != nil {
return applied, fmt.Errorf("reading %s: %w", f, err)
}
_, err = db.Exec(string(data))
if err != nil {
return applied, fmt.Errorf("executing %s: %w", f, err)
}
applied = append(applied, f)
}
return applied, nil
}