// 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