1 Commits

Author SHA1 Message Date
4d6d2f1d62 feat(postgres)!: promote to v1.0.0 — BeginTx with pgx options, Stats, bump all deps to v1.0.0
Add BeginTx(ctx, pgx.TxOptions) to Client interface for explicit isolation level and
read-only transaction control; Begin refactored as a convenience wrapper calling
BeginTx(ctx, pgx.TxOptions{}). Add Stats() *pgxpool.Stat to Component interface for
connection pool observability. Bump all micro-lib dependencies (logz, health, launcher,
xerrors) from v0.9.0 to v1.0.0. API committed as stable.
2026-05-11 19:36:47 -06:00
5 changed files with 71 additions and 17 deletions

View File

@@ -5,6 +5,25 @@ All notable changes to this module will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] — 2026-05-12
### Added
- `Client.BeginTx(ctx context.Context, opts pgx.TxOptions) (Tx, error)` — starts a transaction with explicit isolation level and read-only options using pgx-native `pgx.TxOptions`.
- `Component.Stats() *pgxpool.Stat` — returns connection pool metrics (`TotalConns`, `IdleConns`, `AcquiredConns`, `MaxConns`, etc.) for observability and alerting; returns a zero-value `*pgxpool.Stat` when called before `OnInit`.
### Changed
- `Client.Begin(ctx context.Context) (Tx, error)` — refactored as a convenience wrapper calling `BeginTx(ctx, pgx.TxOptions{})`; behavior is identical, no breaking change.
- All micro-lib dependencies bumped from v0.9.0 to v1.0.0: `logz`, `health`, `launcher`, `xerrors`.
### Unchanged
All other API (`Executor`, `Tx`, `Client`, `Component`, `UnitOfWork`, `Config`, `New`,
`NewUnitOfWork`, `HandleError`) is API-compatible with v0.9.0.
[1.0.0]: https://code.nochebuena.dev/go/postgres/releases/tag/v1.0.0
## [0.9.0] - 2026-03-18 ## [0.9.0] - 2026-03-18
### Added ### Added

8
go.mod
View File

@@ -3,10 +3,10 @@ module code.nochebuena.dev/go/postgres
go 1.25 go 1.25
require ( require (
code.nochebuena.dev/go/health v0.9.0 code.nochebuena.dev/go/health v1.0.0
code.nochebuena.dev/go/launcher v0.9.0 code.nochebuena.dev/go/launcher v1.0.0
code.nochebuena.dev/go/logz v0.9.0 code.nochebuena.dev/go/logz v1.0.0
code.nochebuena.dev/go/xerrors v0.9.0 code.nochebuena.dev/go/xerrors v1.0.0
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6
github.com/jackc/pgx/v5 v5.8.0 github.com/jackc/pgx/v5 v5.8.0
) )

16
go.sum
View File

@@ -1,11 +1,11 @@
code.nochebuena.dev/go/health v0.9.0 h1:x0UKjC7CHAE3AgwyFzCyjmGJIjoLBBxeOHxXuqpbKwI= code.nochebuena.dev/go/health v1.0.0 h1:MOlvrTj8Go0sVgczo1O68nBplZ2DM9Td4aBJqL4HI10=
code.nochebuena.dev/go/health v0.9.0/go.mod h1:f3IsNtU60JSn5yXmBBh9XOvr5pRyEah5+wS4tjDQZso= code.nochebuena.dev/go/health v1.0.0/go.mod h1:f3IsNtU60JSn5yXmBBh9XOvr5pRyEah5+wS4tjDQZso=
code.nochebuena.dev/go/launcher v0.9.0 h1:dJHonA9Xm03AQKK0919FJaQn9ZKHZ+RZfB9yxjnx3TA= code.nochebuena.dev/go/launcher v1.0.0 h1:Jwqdc/1XX9do5CIzZpayC8wqhqiHsbiYJSIrLIpoaZ4=
code.nochebuena.dev/go/launcher v0.9.0/go.mod h1:IBtntmbnyddukjEhxlc7Ysdzz9nZsnd9+8FzAIHt77g= code.nochebuena.dev/go/launcher v1.0.0/go.mod h1:gD2D+aPKfsKNUsT6YkvjszB2fy0qAwvBRXVAtWa4mxo=
code.nochebuena.dev/go/logz v0.9.0 h1:wfV7vtI4V/8ED7Hm31Fbql7Y5iOGrlHN4X8Z5ajTZZE= code.nochebuena.dev/go/logz v1.0.0 h1:DpNvLuVFqyLSVKxaRa799sG8RpHnm1j6dhu4pKiFOvY=
code.nochebuena.dev/go/logz v0.9.0/go.mod h1:qODhSbKb+tWE7rdhHLcKweiP5CgwIaWoZxadCT3bQV8= code.nochebuena.dev/go/logz v1.0.0/go.mod h1:qODhSbKb+tWE7rdhHLcKweiP5CgwIaWoZxadCT3bQV8=
code.nochebuena.dev/go/xerrors v0.9.0 h1:8wrDto7e44ZW1YPOnT6JrxYXTqnvNuKpAO1/5bcT4TE= code.nochebuena.dev/go/xerrors v1.0.0 h1:si24SFGa7cHwAxbu75AAEB+a3qRmF118F/BM2SFI7VI=
code.nochebuena.dev/go/xerrors v0.9.0/go.mod h1:mtXo7xscBreCB7w7smlBP5Onv8H1HVohCvF0I/VXbAY= code.nochebuena.dev/go/xerrors v1.0.0/go.mod h1:mtXo7xscBreCB7w7smlBP5Onv8H1HVohCvF0I/VXbAY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@@ -36,6 +36,7 @@ type Client interface {
// otherwise returns the pool. // otherwise returns the pool.
GetExecutor(ctx context.Context) Executor GetExecutor(ctx context.Context) Executor
Begin(ctx context.Context) (Tx, error) Begin(ctx context.Context) (Tx, error)
BeginTx(ctx context.Context, opts pgx.TxOptions) (Tx, error)
Ping(ctx context.Context) error Ping(ctx context.Context) error
HandleError(err error) error HandleError(err error) error
} }
@@ -45,6 +46,7 @@ type Component interface {
launcher.Component launcher.Component
health.Checkable health.Checkable
Client Client
Stats() *pgxpool.Stat
} }
// UnitOfWork wraps operations in a single database transaction. // UnitOfWork wraps operations in a single database transaction.
@@ -159,20 +161,34 @@ func (c *pgComponent) GetExecutor(ctx context.Context) Executor {
return pool return pool
} }
func (c *pgComponent) Begin(ctx context.Context) (Tx, error) { func (c *pgComponent) BeginTx(ctx context.Context, opts pgx.TxOptions) (Tx, error) {
c.mu.RLock() c.mu.RLock()
pool := c.pool pool := c.pool
c.mu.RUnlock() c.mu.RUnlock()
if pool == nil { if pool == nil {
return nil, fmt.Errorf("postgres: pool not initialized") return nil, fmt.Errorf("postgres: pool not initialized")
} }
tx, err := pool.Begin(ctx) tx, err := pool.BeginTx(ctx, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &pgTx{Tx: tx}, nil return &pgTx{Tx: tx}, nil
} }
func (c *pgComponent) Begin(ctx context.Context) (Tx, error) {
return c.BeginTx(ctx, pgx.TxOptions{})
}
func (c *pgComponent) Stats() *pgxpool.Stat {
c.mu.RLock()
pool := c.pool
c.mu.RUnlock()
if pool == nil {
return &pgxpool.Stat{}
}
return pool.Stat()
}
func (c *pgComponent) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { func (c *pgComponent) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) {
c.mu.RLock() c.mu.RLock()
pool := c.pool pool := c.pool

View File

@@ -105,9 +105,10 @@ func (m *mockTx) Rollback(ctx context.Context) error
type mockClient struct{ tx *mockTx } type mockClient struct{ tx *mockTx }
func (m *mockClient) Begin(ctx context.Context) (Tx, error) { return m.tx, nil } func (m *mockClient) Begin(ctx context.Context) (Tx, error) { return m.tx, nil }
func (m *mockClient) Ping(ctx context.Context) error { return nil } func (m *mockClient) BeginTx(ctx context.Context, opts pgx.TxOptions) (Tx, error) { return m.tx, nil }
func (m *mockClient) HandleError(err error) error { return HandleError(err) } func (m *mockClient) Ping(ctx context.Context) error { return nil }
func (m *mockClient) HandleError(err error) error { return HandleError(err) }
func (m *mockClient) GetExecutor(ctx context.Context) Executor { func (m *mockClient) GetExecutor(ctx context.Context) Executor {
if tx, ok := ctx.Value(ctxTxKey{}).(Executor); ok { if tx, ok := ctx.Value(ctxTxKey{}).(Executor); ok {
return tx return tx
@@ -149,6 +150,24 @@ func TestUnitOfWork_InjectsExecutor(t *testing.T) {
} }
} }
// --- BeginTx / Stats ---
func TestComponent_BeginTx_NilPool(t *testing.T) {
c := &pgComponent{logger: newLogger()}
_, err := c.BeginTx(context.Background(), pgx.TxOptions{})
if err == nil {
t.Error("expected error for nil pool")
}
}
func TestComponent_Stats_NilPool(t *testing.T) {
c := &pgComponent{logger: newLogger()}
stats := c.Stats()
if stats == nil {
t.Error("Stats() should return non-nil zero value when pool is nil")
}
}
// --- helpers --- // --- helpers ---
func assertCode(t *testing.T, err error, want xerrors.Code) { func assertCode(t *testing.T, err error, want xerrors.Code) {