| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- // Command seed applies migrations and the seed file to a fresh
- // Postgres. Idempotent — safe to re-run.
- //
- // Usage:
- //
- // BA_POSTGRES_DSN=postgres://... BA_MIGRATIONS_DIR=./migrations \
- // go run ./cmd/seed
- //
- // In docker-compose this is a one-shot sidecar that runs before
- // the app services start.
- package main
- import (
- "context"
- "fmt"
- "os"
- "path/filepath"
- "sort"
- "strings"
- "time"
- "git3.techno-world.net/lrosales/broad-announce/internal/postgres"
- )
- func main() {
- dsn := os.Getenv("BA_POSTGRES_DSN")
- if dsn == "" {
- die("BA_POSTGRES_DSN is required")
- }
- dir := os.Getenv("BA_MIGRATIONS_DIR")
- if dir == "" {
- die("BA_MIGRATIONS_DIR is required")
- }
- ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
- defer cancel()
- pool, err := postgres.Connect(ctx, dsn)
- if err != nil {
- die("connect: " + err.Error())
- }
- defer pool.Close()
- if err := applyDir(ctx, pool, dir, "*.up.sql"); err != nil {
- die("apply migrations: " + err.Error())
- }
- seed := filepath.Join(dir, "seed.sql")
- if _, err := os.Stat(seed); err == nil {
- if err := execFile(ctx, pool, seed); err != nil {
- die("seed: " + err.Error())
- }
- fmt.Fprintln(os.Stderr, "seed applied:", seed)
- } else {
- fmt.Fprintln(os.Stderr, "no seed.sql in", dir)
- }
- fmt.Fprintln(os.Stderr, "ok")
- }
- func applyDir(ctx context.Context, pool *postgres.Pool, dir, pattern string) error {
- ups, err := filepath.Glob(filepath.Join(dir, pattern))
- if err != nil {
- return err
- }
- sort.Strings(ups)
- for _, p := range ups {
- if err := execFile(ctx, pool, p); err != nil {
- return fmt.Errorf("%s: %w", filepath.Base(p), err)
- }
- fmt.Fprintln(os.Stderr, "applied:", filepath.Base(p))
- }
- return nil
- }
- func execFile(ctx context.Context, pool *postgres.Pool, path string) error {
- body, err := os.ReadFile(path)
- if err != nil {
- return err
- }
- // pgx's Pool.Exec supports multi-statement SQL when the
- // underlying connection's protocol-mode supports it. To be
- // safe we acquire a single connection and exec the whole file
- // as one batch. If the file contains a CREATE EXTENSION that
- // requires superuser, the migration container runs as superuser
- // so this is fine.
- conn, err := pool.Acquire(ctx)
- if err != nil {
- return fmt.Errorf("acquire conn: %w", err)
- }
- defer conn.Release()
- _, err = conn.Exec(ctx, string(body))
- return err
- }
- func die(msg string) {
- fmt.Fprintln(os.Stderr, "seed:", msg)
- os.Exit(1)
- }
- // _ silences unused import for build tags.
- var _ = strings.TrimSpace
|