dana-medical-site/internal/storage/storage.go
2026-10-14 13:33:13 +00:00

106 lines
2.4 KiB
Go

package storage
import (
"context"
"embed"
"fmt"
"log/slog"
"sort"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
)
//go:embed migrations/*.sql
var migrationFS embed.FS
type Storage struct {
pool *pgxpool.Pool
}
func New(ctx context.Context, dsn string) (*Storage, error) {
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping: %w", err)
}
return &Storage{pool: pool}, nil
}
func (s *Storage) Close() { s.pool.Close() }
func (s *Storage) Pool() *pgxpool.Pool { return s.pool }
// Migrate применяет sql-файлы из migrations по порядку имён.
// Каждый файл выполняется в транзакции и отмечается в schema_migrations.
func (s *Storage) Migrate(ctx context.Context) error {
_, err := s.pool.Exec(ctx, `
CREATE TABLE IF NOT EXISTS schema_migrations (
version text PRIMARY KEY,
applied_at timestamptz NOT NULL DEFAULT now()
)`)
if err != nil {
return fmt.Errorf("create schema_migrations: %w", err)
}
applied := map[string]bool{}
rows, err := s.pool.Query(ctx, `SELECT version FROM schema_migrations`)
if err != nil {
return fmt.Errorf("read applied: %w", err)
}
for rows.Next() {
var v string
if err := rows.Scan(&v); err != nil {
rows.Close()
return err
}
applied[v] = true
}
rows.Close()
if err := rows.Err(); err != nil {
return err
}
entries, err := migrationFS.ReadDir("migrations")
if err != nil {
return err
}
names := make([]string, 0, len(entries))
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
names = append(names, e.Name())
}
}
sort.Strings(names)
for _, name := range names {
if applied[name] {
continue
}
body, err := migrationFS.ReadFile("migrations/" + name)
if err != nil {
return err
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
if _, err := tx.Exec(ctx, string(body)); err != nil {
_ = tx.Rollback(ctx)
return fmt.Errorf("migration %s: %w", name, err)
}
if _, err := tx.Exec(ctx, `INSERT INTO schema_migrations (version) VALUES ($1)`, name); err != nil {
_ = tx.Rollback(ctx)
return err
}
if err := tx.Commit(ctx); err != nil {
return err
}
slog.Info("migration applied", "file", name)
}
return nil
}