| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 |
- // 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 files run in lexical order, so seed.sql → seed_m2.sql
- // → seed_m3.sql → … Each is idempotent (ON CONFLICT DO NOTHING)
- // and safe to re-run.
- for _, name := range []string{"seed.sql", "seed_m2.sql"} {
- p := filepath.Join(dir, name)
- if _, err := os.Stat(p); err != nil {
- fmt.Fprintln(os.Stderr, "no", name, "in", dir, "(skipped)")
- continue
- }
- if err := execFile(ctx, pool, p); err != nil {
- die(name+": " + err.Error())
- }
- fmt.Fprintln(os.Stderr, "applied:", name)
- }
- 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
|