2026-09-29 13:47:26 +08:00
|
|
|
package database
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
_ "embed"
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//go:embed 001_practice.sql
|
|
|
|
|
var practiceSQL string
|
|
|
|
|
|
2026-09-29 18:40:25 +08:00
|
|
|
//go:embed 002_project_icons.sql
|
|
|
|
|
var projectIconsSQL string
|
|
|
|
|
|
|
|
|
|
//go:embed 003_project_order.sql
|
|
|
|
|
var projectOrderSQL string
|
|
|
|
|
|
2026-09-29 13:47:26 +08:00
|
|
|
// Migrate applies versioned migrations atomically. A transaction-scoped lock keeps
|
|
|
|
|
// concurrent service starts from applying the same version twice.
|
|
|
|
|
func Migrate(ctx context.Context, pool *pgxpool.Pool) error {
|
|
|
|
|
tx, err := pool.Begin(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer tx.Rollback(ctx)
|
|
|
|
|
if _, err = tx.Exec(ctx, "SELECT pg_advisory_xact_lock(684127930)"); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if _, err = tx.Exec(ctx, "CREATE TABLE IF NOT EXISTS schema_migrations (version integer PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())"); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-09-29 18:40:25 +08:00
|
|
|
for i, sql := range []string{practiceSQL, projectIconsSQL, projectOrderSQL} {
|
|
|
|
|
version := i + 1
|
|
|
|
|
var applied bool
|
|
|
|
|
if err = tx.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version=$1)", version).Scan(&applied); err != nil {
|
2026-09-29 13:47:26 +08:00
|
|
|
return err
|
|
|
|
|
}
|
2026-09-29 18:40:25 +08:00
|
|
|
if !applied {
|
|
|
|
|
if _, err = tx.Exec(ctx, sql); err != nil {
|
|
|
|
|
return fmt.Errorf("migration %d: %w", version, err)
|
|
|
|
|
}
|
|
|
|
|
if _, err = tx.Exec(ctx, "INSERT INTO schema_migrations(version) VALUES ($1)", version); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-29 13:47:26 +08:00
|
|
|
}
|
|
|
|
|
return tx.Commit(ctx)
|
|
|
|
|
}
|