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.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package pg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
// brokenFS fails every Open, so fs.ReadDir(".") fails.
|
||||
type brokenFS struct{}
|
||||
|
||||
func (brokenFS) Open(string) (fs.File, error) { return nil, fs.ErrPermission }
|
||||
|
||||
// missingFileFS lists a migration that cannot then be read, the shape a
|
||||
// mis-built embed or a racing file deletion produces.
|
||||
type missingFileFS struct{}
|
||||
|
||||
func (missingFileFS) Open(string) (fs.File, error) { return nil, fs.ErrNotExist }
|
||||
|
||||
func (missingFileFS) ReadDir(string) ([]fs.DirEntry, error) {
|
||||
return []fs.DirEntry{fakeDirEntry{name: "0001_first.sql"}}, nil
|
||||
}
|
||||
|
||||
type fakeDirEntry struct {
|
||||
name string
|
||||
dir bool
|
||||
}
|
||||
|
||||
func (e fakeDirEntry) Name() string { return e.name }
|
||||
func (e fakeDirEntry) IsDir() bool { return e.dir }
|
||||
func (e fakeDirEntry) Type() fs.FileMode {
|
||||
if e.dir {
|
||||
return fs.ModeDir
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func (e fakeDirEntry) Info() (fs.FileInfo, error) { return nil, errors.New("no info") }
|
||||
|
||||
// execCall records one statement the migrator issued.
|
||||
type execCall struct {
|
||||
sql string
|
||||
args []any
|
||||
}
|
||||
|
||||
// fakeSession is a pgx connection that records statements instead of running
|
||||
// them. Embedding nothing: it implements the whole session interface.
|
||||
type fakeSession struct {
|
||||
execs []execCall
|
||||
execErrs map[string]error
|
||||
|
||||
queries []string
|
||||
rows *fakeRows
|
||||
queryErr error
|
||||
|
||||
tx *fakeTx
|
||||
beginErr error
|
||||
}
|
||||
|
||||
func (s *fakeSession) Exec(_ context.Context, sql string, args ...any) (pgconn.CommandTag, error) {
|
||||
s.execs = append(s.execs, execCall{sql: sql, args: args})
|
||||
return pgconn.NewCommandTag("SELECT 1"), s.execErrs[sql]
|
||||
}
|
||||
|
||||
func (s *fakeSession) Query(_ context.Context, sql string, _ ...any) (pgx.Rows, error) {
|
||||
s.queries = append(s.queries, sql)
|
||||
if s.queryErr != nil {
|
||||
return nil, s.queryErr
|
||||
}
|
||||
return s.rows, nil
|
||||
}
|
||||
|
||||
func (s *fakeSession) Begin(context.Context) (pgx.Tx, error) {
|
||||
if s.beginErr != nil {
|
||||
return nil, s.beginErr
|
||||
}
|
||||
if s.tx == nil {
|
||||
s.tx = &fakeTx{}
|
||||
}
|
||||
return s.tx, nil
|
||||
}
|
||||
|
||||
// execSQL returns just the statements issued, in order.
|
||||
func (s *fakeSession) execSQL() []string {
|
||||
out := make([]string, 0, len(s.execs))
|
||||
for _, c := range s.execs {
|
||||
out = append(out, c.sql)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// fakeRows serves a fixed column of strings. The embedded interface supplies
|
||||
// the pgx.Rows methods the migrator never calls; calling one panics, which is
|
||||
// the intent — it would mean the migrator grew an untested dependency.
|
||||
type fakeRows struct {
|
||||
pgx.Rows
|
||||
|
||||
values []string
|
||||
i int
|
||||
scanErr error
|
||||
err error
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (r *fakeRows) Next() bool {
|
||||
if r.i >= len(r.values) {
|
||||
return false
|
||||
}
|
||||
r.i++
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *fakeRows) Scan(dest ...any) error {
|
||||
if r.scanErr != nil {
|
||||
return r.scanErr
|
||||
}
|
||||
if len(dest) != 1 {
|
||||
return errors.New("expected exactly one scan destination")
|
||||
}
|
||||
p, ok := dest[0].(*string)
|
||||
if !ok {
|
||||
return errors.New("expected a *string scan destination")
|
||||
}
|
||||
*p = r.values[r.i-1]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeRows) Close() { r.closed = true }
|
||||
func (r *fakeRows) Err() error { return r.err }
|
||||
|
||||
// fakeTx records the statements and the terminal call of a transaction.
|
||||
type fakeTx struct {
|
||||
pgx.Tx
|
||||
|
||||
execs []execCall
|
||||
execErrs map[string]error
|
||||
commitErr error
|
||||
committed bool
|
||||
rolled int
|
||||
}
|
||||
|
||||
func (t *fakeTx) Exec(_ context.Context, sql string, args ...any) (pgconn.CommandTag, error) {
|
||||
t.execs = append(t.execs, execCall{sql: sql, args: args})
|
||||
return pgconn.NewCommandTag("INSERT 0 1"), t.execErrs[sql]
|
||||
}
|
||||
|
||||
func (t *fakeTx) Commit(context.Context) error {
|
||||
if t.commitErr != nil {
|
||||
return t.commitErr
|
||||
}
|
||||
t.committed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *fakeTx) Rollback(context.Context) error {
|
||||
t.rolled++
|
||||
if t.committed {
|
||||
// What pgx returns for a rollback after a successful commit; the
|
||||
// deferred rollback in Apply must tolerate it.
|
||||
return pgx.ErrTxClosed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *fakeTx) execSQL() []string {
|
||||
out := make([]string, 0, len(t.execs))
|
||||
for _, c := range t.execs {
|
||||
out = append(out, c.sql)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user