package store import ( "context" "embed" "fmt" "io/fs" "github.com/jackc/pgx/v5/pgxpool" ) //go:embed migrations/*.sql var migrationsFS embed.FS // migrate applies every embedded migration not yet recorded in // schema_migrations, in file-name order, each in its own transaction. func migrate(ctx context.Context, pool *pgxpool.Pool) error { 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 := fs.ReadDir(migrationsFS, "migrations") if err != nil { return fmt.Errorf("read migrations: %w", err) } for _, e := range entries { name := e.Name() var applied bool if err := pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM schema_migrations WHERE version = $1)`, name, ).Scan(&applied); err != nil { return fmt.Errorf("check migration %s: %w", name, err) } if applied { continue } sql, err := migrationsFS.ReadFile("migrations/" + name) if err != nil { return fmt.Errorf("read migration %s: %w", name, err) } if err := applyMigration(ctx, pool, name, string(sql)); err != nil { return err } } return nil } func applyMigration(ctx context.Context, pool *pgxpool.Pool, name, sql string) error { tx, err := pool.Begin(ctx) if err != nil { return fmt.Errorf("begin migration %s: %w", name, err) } defer func() { _ = tx.Rollback(ctx) }() // Exec without arguments uses the simple protocol, which allows the // several statements a migration file contains. if _, err := tx.Exec(ctx, sql); err != nil { return fmt.Errorf("migration %s: %w", name, err) } if _, err := tx.Exec(ctx, `INSERT INTO schema_migrations (version) VALUES ($1)`, name); err != nil { return fmt.Errorf("record migration %s: %w", name, err) } if err := tx.Commit(ctx); err != nil { return fmt.Errorf("commit migration %s: %w", name, err) } return nil }