mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
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:
202
server/migrations/migrations_test.go
Normal file
202
server/migrations/migrations_test.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func testDB(t *testing.T) *sqlx.DB {
|
||||
t.Helper()
|
||||
|
||||
host := getEnv("TEST_DB_HOST", "localhost")
|
||||
port := getEnv("TEST_DB_PORT", "5433")
|
||||
user := getEnv("TEST_DB_USER", "test")
|
||||
password := getEnv("TEST_DB_PASSWORD", "test")
|
||||
dbName := getEnv("TEST_DB_NAME", "erupe_test")
|
||||
|
||||
connStr := fmt.Sprintf(
|
||||
"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||
host, port, user, password, dbName,
|
||||
)
|
||||
|
||||
db, err := sqlx.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
t.Skipf("Test database not available: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
_ = db.Close()
|
||||
t.Skipf("Test database not available: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clean slate
|
||||
_, err = db.Exec("DROP SCHEMA public CASCADE; CREATE SCHEMA public;")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to clean database: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func TestMigrateEmptyDB(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
applied, err := Migrate(db, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("Migrate failed: %v", err)
|
||||
}
|
||||
if applied != 1 {
|
||||
t.Errorf("expected 1 migration applied, got %d", applied)
|
||||
}
|
||||
|
||||
ver, err := Version(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Version failed: %v", err)
|
||||
}
|
||||
if ver != 1 {
|
||||
t.Errorf("expected version 1, got %d", ver)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateAlreadyMigrated(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
// First run
|
||||
_, err := Migrate(db, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("First Migrate failed: %v", err)
|
||||
}
|
||||
|
||||
// Second run should apply 0
|
||||
applied, err := Migrate(db, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("Second Migrate failed: %v", err)
|
||||
}
|
||||
if applied != 0 {
|
||||
t.Errorf("expected 0 migrations on second run, got %d", applied)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateExistingDBWithoutSchemaVersion(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
// Simulate an existing database: create a dummy table
|
||||
_, err := db.Exec("CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create dummy table: %v", err)
|
||||
}
|
||||
|
||||
// Migrate should detect existing DB and auto-mark baseline
|
||||
applied, err := Migrate(db, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("Migrate failed: %v", err)
|
||||
}
|
||||
// Baseline (0001) is auto-marked, so 0 "new" migrations applied
|
||||
if applied != 0 {
|
||||
t.Errorf("expected 0 migrations applied (baseline auto-marked), got %d", applied)
|
||||
}
|
||||
|
||||
ver, err := Version(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Version failed: %v", err)
|
||||
}
|
||||
if ver != 1 {
|
||||
t.Errorf("expected version 1 (auto-marked baseline), got %d", ver)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionEmptyDB(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
ver, err := Version(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Version failed: %v", err)
|
||||
}
|
||||
if ver != 0 {
|
||||
t.Errorf("expected version 0 on empty DB, got %d", ver)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplySeedData(t *testing.T) {
|
||||
db := testDB(t)
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
// Apply schema first
|
||||
_, err := Migrate(db, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("Migrate failed: %v", err)
|
||||
}
|
||||
|
||||
count, err := ApplySeedData(db, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplySeedData failed: %v", err)
|
||||
}
|
||||
if count == 0 {
|
||||
t.Error("expected at least 1 seed file applied, got 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{"0001_init.sql", 1, false},
|
||||
{"0002_add_users.sql", 2, false},
|
||||
{"0100_big_change.sql", 100, false},
|
||||
{"bad.sql", 0, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := parseVersion(tt.filename)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseVersion(%q) error = %v, wantErr %v", tt.filename, err, tt.wantErr)
|
||||
continue
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("parseVersion(%q) = %d, want %d", tt.filename, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMigrations(t *testing.T) {
|
||||
migrations, err := readMigrations()
|
||||
if err != nil {
|
||||
t.Fatalf("readMigrations failed: %v", err)
|
||||
}
|
||||
if len(migrations) == 0 {
|
||||
t.Fatal("expected at least 1 migration, got 0")
|
||||
}
|
||||
if migrations[0].version != 1 {
|
||||
t.Errorf("first migration version = %d, want 1", migrations[0].version)
|
||||
}
|
||||
if migrations[0].filename != "0001_init.sql" {
|
||||
t.Errorf("first migration filename = %q, want 0001_init.sql", migrations[0].filename)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user