panel v0.9.1 — open-source game server manager
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
// migrateLockKey is the pg advisory-lock key serializing Migrate across
|
||||
// controllers sharing one database. Arbitrary but fixed: fnv-ish of
|
||||
// "panel:schema_migrations". Session-scoped (not transaction-scoped) so it
|
||||
// spans the whole multi-transaction run and releases on disconnect if the
|
||||
// process dies mid-migrate.
|
||||
const migrateLockKey int64 = 0x70616e656c6d6967 // "panelmig"
|
||||
|
||||
// Migrate applies any pending SQL files in migrations/ in filename-sorted
|
||||
// order. Each file is wrapped in a transaction alongside its schema_migrations
|
||||
// insert so failure leaves the DB in a consistent state. A session-level
|
||||
// pg advisory lock serializes concurrent controllers: the second one blocks
|
||||
// here, then finds every version already recorded and no-ops.
|
||||
func Migrate(ctx context.Context, pool *pgxpool.Pool) error {
|
||||
// Dedicated connection: session advisory locks belong to a connection,
|
||||
// and pool.Exec could lock on one conn and unlock on another.
|
||||
lockConn, err := pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("acquire migrate lock conn: %w", err)
|
||||
}
|
||||
defer lockConn.Release()
|
||||
if _, err := lockConn.Exec(ctx, `SELECT pg_advisory_lock($1)`, migrateLockKey); err != nil {
|
||||
return fmt.Errorf("acquire migrate advisory lock: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = lockConn.Exec(context.WithoutCancel(ctx), `SELECT pg_advisory_unlock($1)`, migrateLockKey)
|
||||
}()
|
||||
|
||||
if _, err := pool.Exec(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version TEXT PRIMARY KEY,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)`); err != nil {
|
||||
return fmt.Errorf("create schema_migrations: %w", err)
|
||||
}
|
||||
|
||||
entries, err := migrationsFS.ReadDir("migrations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("read embedded migrations: %w", err)
|
||||
}
|
||||
var files []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
|
||||
files = append(files, e.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(files)
|
||||
|
||||
for _, name := range files {
|
||||
var exists bool
|
||||
if err := pool.QueryRow(ctx,
|
||||
`SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version = $1)`, name,
|
||||
).Scan(&exists); err != nil {
|
||||
return fmt.Errorf("check %s: %w", name, err)
|
||||
}
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
|
||||
body, err := migrationsFS.ReadFile("migrations/" + name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", name, err)
|
||||
}
|
||||
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx for %s: %w", name, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, string(body)); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return fmt.Errorf("apply %s: %w", name, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO schema_migrations(version) VALUES ($1)`, name); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return fmt.Errorf("record %s: %w", name, err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("commit %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user