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 }