diff --git a/CHANGELOG.md b/CHANGELOG.md index d692476..1591567 100644 --- a/CHANGELOG.md +++ b/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/), 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 ### Added diff --git a/go.mod b/go.mod index 775c0e0..0f34172 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module code.nochebuena.dev/go/postgres go 1.25 require ( - code.nochebuena.dev/go/health v0.9.0 - code.nochebuena.dev/go/launcher v0.9.0 - code.nochebuena.dev/go/logz v0.9.0 - code.nochebuena.dev/go/xerrors v0.9.0 + code.nochebuena.dev/go/health v1.0.0 + code.nochebuena.dev/go/launcher v1.0.0 + code.nochebuena.dev/go/logz v1.0.0 + code.nochebuena.dev/go/xerrors v1.0.0 github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 github.com/jackc/pgx/v5 v5.8.0 ) diff --git a/go.sum b/go.sum index a327c8c..517214c 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ -code.nochebuena.dev/go/health v0.9.0 h1:x0UKjC7CHAE3AgwyFzCyjmGJIjoLBBxeOHxXuqpbKwI= -code.nochebuena.dev/go/health v0.9.0/go.mod h1:f3IsNtU60JSn5yXmBBh9XOvr5pRyEah5+wS4tjDQZso= -code.nochebuena.dev/go/launcher v0.9.0 h1:dJHonA9Xm03AQKK0919FJaQn9ZKHZ+RZfB9yxjnx3TA= -code.nochebuena.dev/go/launcher v0.9.0/go.mod h1:IBtntmbnyddukjEhxlc7Ysdzz9nZsnd9+8FzAIHt77g= -code.nochebuena.dev/go/logz v0.9.0 h1:wfV7vtI4V/8ED7Hm31Fbql7Y5iOGrlHN4X8Z5ajTZZE= -code.nochebuena.dev/go/logz v0.9.0/go.mod h1:qODhSbKb+tWE7rdhHLcKweiP5CgwIaWoZxadCT3bQV8= -code.nochebuena.dev/go/xerrors v0.9.0 h1:8wrDto7e44ZW1YPOnT6JrxYXTqnvNuKpAO1/5bcT4TE= -code.nochebuena.dev/go/xerrors v0.9.0/go.mod h1:mtXo7xscBreCB7w7smlBP5Onv8H1HVohCvF0I/VXbAY= +code.nochebuena.dev/go/health v1.0.0 h1:MOlvrTj8Go0sVgczo1O68nBplZ2DM9Td4aBJqL4HI10= +code.nochebuena.dev/go/health v1.0.0/go.mod h1:f3IsNtU60JSn5yXmBBh9XOvr5pRyEah5+wS4tjDQZso= +code.nochebuena.dev/go/launcher v1.0.0 h1:Jwqdc/1XX9do5CIzZpayC8wqhqiHsbiYJSIrLIpoaZ4= +code.nochebuena.dev/go/launcher v1.0.0/go.mod h1:gD2D+aPKfsKNUsT6YkvjszB2fy0qAwvBRXVAtWa4mxo= +code.nochebuena.dev/go/logz v1.0.0 h1:DpNvLuVFqyLSVKxaRa799sG8RpHnm1j6dhu4pKiFOvY= +code.nochebuena.dev/go/logz v1.0.0/go.mod h1:qODhSbKb+tWE7rdhHLcKweiP5CgwIaWoZxadCT3bQV8= +code.nochebuena.dev/go/xerrors v1.0.0 h1:si24SFGa7cHwAxbu75AAEB+a3qRmF118F/BM2SFI7VI= +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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/postgres.go b/postgres.go index 3c5d4f5..b73733f 100644 --- a/postgres.go +++ b/postgres.go @@ -36,6 +36,7 @@ type Client interface { // otherwise returns the pool. GetExecutor(ctx context.Context) Executor Begin(ctx context.Context) (Tx, error) + BeginTx(ctx context.Context, opts pgx.TxOptions) (Tx, error) Ping(ctx context.Context) error HandleError(err error) error } @@ -45,6 +46,7 @@ type Component interface { launcher.Component health.Checkable Client + Stats() *pgxpool.Stat } // UnitOfWork wraps operations in a single database transaction. @@ -159,20 +161,34 @@ func (c *pgComponent) GetExecutor(ctx context.Context) Executor { 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() pool := c.pool c.mu.RUnlock() if pool == nil { return nil, fmt.Errorf("postgres: pool not initialized") } - tx, err := pool.Begin(ctx) + tx, err := pool.BeginTx(ctx, opts) if err != nil { return nil, err } 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) { c.mu.RLock() pool := c.pool diff --git a/postgres_test.go b/postgres_test.go index b110298..8258d03 100644 --- a/postgres_test.go +++ b/postgres_test.go @@ -105,9 +105,10 @@ func (m *mockTx) Rollback(ctx context.Context) error type mockClient struct{ tx *mockTx } -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) HandleError(err error) error { return HandleError(err) } +func (m *mockClient) Begin(ctx context.Context) (Tx, error) { return m.tx, nil } +func (m *mockClient) BeginTx(ctx context.Context, opts pgx.TxOptions) (Tx, error) { return m.tx, nil } +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 { if tx, ok := ctx.Value(ctxTxKey{}).(Executor); ok { 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 --- func assertCode(t *testing.T, err error, want xerrors.Code) {