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.
This commit is contained in:
19
CHANGELOG.md
19
CHANGELOG.md
@@ -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
8
go.mod
@@ -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
16
go.sum
@@ -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=
|
||||||
|
|||||||
20
postgres.go
20
postgres.go
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user