main.go 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. // Command seed applies migrations and the seed file to a fresh
  2. // Postgres. Idempotent — safe to re-run.
  3. //
  4. // Usage:
  5. //
  6. // BA_POSTGRES_DSN=postgres://... BA_MIGRATIONS_DIR=./migrations \
  7. // go run ./cmd/seed
  8. //
  9. // In docker-compose this is a one-shot sidecar that runs before
  10. // the app services start.
  11. package main
  12. import (
  13. "context"
  14. "fmt"
  15. "os"
  16. "path/filepath"
  17. "sort"
  18. "strings"
  19. "time"
  20. "git3.techno-world.net/lrosales/broad-announce/internal/postgres"
  21. )
  22. func main() {
  23. dsn := os.Getenv("BA_POSTGRES_DSN")
  24. if dsn == "" {
  25. die("BA_POSTGRES_DSN is required")
  26. }
  27. dir := os.Getenv("BA_MIGRATIONS_DIR")
  28. if dir == "" {
  29. die("BA_MIGRATIONS_DIR is required")
  30. }
  31. ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
  32. defer cancel()
  33. pool, err := postgres.Connect(ctx, dsn)
  34. if err != nil {
  35. die("connect: " + err.Error())
  36. }
  37. defer pool.Close()
  38. if err := applyDir(ctx, pool, dir, "*.up.sql"); err != nil {
  39. die("apply migrations: " + err.Error())
  40. }
  41. // Seed files run in lexical order, so seed.sql → seed_m2.sql
  42. // → seed_m3.sql → … Each is idempotent (ON CONFLICT DO NOTHING)
  43. // and safe to re-run.
  44. for _, name := range []string{"seed.sql", "seed_m2.sql"} {
  45. p := filepath.Join(dir, name)
  46. if _, err := os.Stat(p); err != nil {
  47. fmt.Fprintln(os.Stderr, "no", name, "in", dir, "(skipped)")
  48. continue
  49. }
  50. if err := execFile(ctx, pool, p); err != nil {
  51. die(name+": " + err.Error())
  52. }
  53. fmt.Fprintln(os.Stderr, "applied:", name)
  54. }
  55. fmt.Fprintln(os.Stderr, "ok")
  56. }
  57. func applyDir(ctx context.Context, pool *postgres.Pool, dir, pattern string) error {
  58. ups, err := filepath.Glob(filepath.Join(dir, pattern))
  59. if err != nil {
  60. return err
  61. }
  62. sort.Strings(ups)
  63. for _, p := range ups {
  64. if err := execFile(ctx, pool, p); err != nil {
  65. return fmt.Errorf("%s: %w", filepath.Base(p), err)
  66. }
  67. fmt.Fprintln(os.Stderr, "applied:", filepath.Base(p))
  68. }
  69. return nil
  70. }
  71. func execFile(ctx context.Context, pool *postgres.Pool, path string) error {
  72. body, err := os.ReadFile(path)
  73. if err != nil {
  74. return err
  75. }
  76. // pgx's Pool.Exec supports multi-statement SQL when the
  77. // underlying connection's protocol-mode supports it. To be
  78. // safe we acquire a single connection and exec the whole file
  79. // as one batch. If the file contains a CREATE EXTENSION that
  80. // requires superuser, the migration container runs as superuser
  81. // so this is fine.
  82. conn, err := pool.Acquire(ctx)
  83. if err != nil {
  84. return fmt.Errorf("acquire conn: %w", err)
  85. }
  86. defer conn.Release()
  87. _, err = conn.Exec(ctx, string(body))
  88. return err
  89. }
  90. func die(msg string) {
  91. fmt.Fprintln(os.Stderr, "seed:", msg)
  92. os.Exit(1)
  93. }
  94. // _ silences unused import for build tags.
  95. var _ = strings.TrimSpace