Files
unkin-agent d59dcbe74e
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Add the golib scaffold and the pg module
Stand up the shared library with its first module: the Postgres plumbing
every service currently copy-pastes — the DSN builder, the pool
constructor, and the migration runner arrproxy proved out.

- Add pg.DSNFromEnv, generalising the identical Sprintf builders in
  encapi, artifactapi and forgebot into one prefixed lookup with
  DATABASE_URL passthrough and libpq fallbacks.
- Add pg.New and pg.NewMigrated, which ping before returning so an
  unreachable server fails at startup rather than on the first query.
- Add pg.Migrate, lifting arrproxy's runner verbatim in semantics and
  generalising the hardcoded advisory-lock key to FNV-1a/64 of a
  caller-supplied name and the embedded set to an fs.FS.
- Add pg/pgtest, unifying the encapi and artifactapi testcontainers
  helpers, with SkipIfShort so container-backed tests self-skip on the
  Docker-less Kubernetes runners.
- Add the Makefile, README and pre-commit config, plus test, pre-commit
  and build pipelines on golib-ci.
2026-08-31 22:19:40 +10:00

45 lines
1.3 KiB
Go

package pg
import (
"context"
"fmt"
"io/fs"
"log/slog"
"github.com/jackc/pgx/v5/pgxpool"
)
// New opens a pgxpool against dsn and verifies it can reach the server before
// returning. pgxpool connects lazily, so without the ping a bad address only
// surfaces on the first query, long after startup has reported success.
//
// The caller owns the pool and must Close it. log may be nil.
func New(ctx context.Context, dsn string, log *slog.Logger) (*pgxpool.Pool, error) {
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
return nil, fmt.Errorf("connect postgres: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping postgres: %w", err)
}
logger(log).Debug("postgres pool ready", "host", pool.Config().ConnConfig.Host)
return pool, nil
}
// NewMigrated opens a pool and brings the schema up to date before returning
// it, so a service cannot start serving against a half-migrated database. A
// migration failure closes the pool and returns the error; callers treat it as
// fatal.
func NewMigrated(ctx context.Context, dsn string, fsys fs.FS, opts MigrateOptions) (*pgxpool.Pool, error) {
pool, err := New(ctx, dsn, opts.Logger)
if err != nil {
return nil, err
}
if err := Migrate(ctx, pool, fsys, opts); err != nil {
pool.Close()
return nil, err
}
return pool, nil
}