2 Commits

Author SHA1 Message Date
3044f2bd00 chore: bump go directive from 1.25 to 1.26 2026-05-12 02:06:46 +00:00
f43fc8056c feat(mysql)!: promote to v1.0.0 — BeginTx with isolation levels, Stats, bump all deps to v1.0.0
Add BeginTx(ctx, *sql.TxOptions) to Client interface for explicit transaction isolation
level control; Begin refactored as a convenience wrapper calling BeginTx(ctx, nil).
Add Stats() sql.DBStats 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-12 01:20:35 +00:00
5 changed files with 74 additions and 19 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 *sql.TxOptions) (Tx, error)` — starts a transaction with explicit isolation level and read-only options; accepts any `*sql.TxOptions` supported by `database/sql`.
- `Component.Stats() sql.DBStats` — returns connection pool metrics (`OpenConnections`, `WaitCount`, `WaitDuration`, etc.) for observability and alerting; returns zero value when called before `OnInit`.
### Changed
- `Client.Begin(ctx context.Context) (Tx, error)` — refactored as a convenience wrapper calling `BeginTx(ctx, nil)`; behavior is identical, no breaking change.
- All micro-lib dependencies bumped from v0.9.1 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.1.
[1.0.0]: https://code.nochebuena.dev/go/mysql/releases/tag/v1.0.0
## [0.9.1] - 2026-03-20 ## [0.9.1] - 2026-03-20
### Added ### Added
@@ -23,6 +42,8 @@ and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.
- **Backward compatible.** Existing `Config` literals that do not set `Charset`, `Loc`, or `ParseTime` produce the same DSN as v0.9.0 (`charset=utf8mb4&loc=UTC&parseTime=true`). - **Backward compatible.** Existing `Config` literals that do not set `Charset`, `Loc`, or `ParseTime` produce the same DSN as v0.9.0 (`charset=utf8mb4&loc=UTC&parseTime=true`).
- **Collation via DSN is not supported.** `go-sql-driver` v1.8.x negotiates the collation using a 1-byte handshake ID (max 255). MariaDB 11.4+ collations such as `utf8mb4_uca1400_as_cs` carry IDs > 255 and will cause a connection error if set in the DSN. Set the desired collation in schema migrations at the database/table level. - **Collation via DSN is not supported.** `go-sql-driver` v1.8.x negotiates the collation using a 1-byte handshake ID (max 255). MariaDB 11.4+ collations such as `utf8mb4_uca1400_as_cs` carry IDs > 255 and will cause a connection error if set in the DSN. Set the desired collation in schema migrations at the database/table level.
[0.9.1]: https://code.nochebuena.dev/go/mysql/releases/tag/v0.9.1
## [0.9.0] - 2026-03-18 ## [0.9.0] - 2026-03-18
### Added ### Added
@@ -49,5 +70,4 @@ and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.
- The module is structurally parallel to `postgres` but uses `database/sql` types throughout; the two modules are intentionally type-incompatible. - The module is structurally parallel to `postgres` but uses `database/sql` types throughout; the two modules are intentionally type-incompatible.
- MySQL error codes are matched by numeric constant via `MySQLError.Number`, not by string parsing, for stability across MySQL and MariaDB versions. - MySQL error codes are matched by numeric constant via `MySQLError.Number`, not by string parsing, for stability across MySQL and MariaDB versions.
[0.9.1]: https://code.nochebuena.dev/go/mysql/releases/tag/v0.9.1
[0.9.0]: https://code.nochebuena.dev/go/mysql/releases/tag/v0.9.0 [0.9.0]: https://code.nochebuena.dev/go/mysql/releases/tag/v0.9.0

10
go.mod
View File

@@ -1,12 +1,12 @@
module code.nochebuena.dev/go/mysql module code.nochebuena.dev/go/mysql
go 1.25 go 1.26
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/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.8.1
) )

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=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=

View File

@@ -33,6 +33,7 @@ type Tx interface {
type Client interface { type Client interface {
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 *sql.TxOptions) (Tx, error)
Ping(ctx context.Context) error Ping(ctx context.Context) error
HandleError(err error) error HandleError(err error) error
} }
@@ -42,6 +43,7 @@ type Component interface {
launcher.Component launcher.Component
health.Checkable health.Checkable
Client Client
Stats() sql.DBStats
} }
// UnitOfWork wraps operations in a single database transaction. // UnitOfWork wraps operations in a single database transaction.
@@ -192,20 +194,34 @@ func (c *mysqlComponent) GetExecutor(ctx context.Context) Executor {
return db return db
} }
func (c *mysqlComponent) Begin(ctx context.Context) (Tx, error) { func (c *mysqlComponent) BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error) {
c.mu.RLock() c.mu.RLock()
db := c.db db := c.db
c.mu.RUnlock() c.mu.RUnlock()
if db == nil { if db == nil {
return nil, fmt.Errorf("mysql: not initialized") return nil, fmt.Errorf("mysql: not initialized")
} }
tx, err := db.BeginTx(ctx, nil) tx, err := db.BeginTx(ctx, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &mysqlTx{Tx: tx}, nil return &mysqlTx{Tx: tx}, nil
} }
func (c *mysqlComponent) Begin(ctx context.Context) (Tx, error) {
return c.BeginTx(ctx, nil)
}
func (c *mysqlComponent) Stats() sql.DBStats {
c.mu.RLock()
db := c.db
c.mu.RUnlock()
if db == nil {
return sql.DBStats{}
}
return db.Stats()
}
func (c *mysqlComponent) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { func (c *mysqlComponent) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
c.mu.RLock() c.mu.RLock()
db := c.db db := c.db

View File

@@ -99,9 +99,10 @@ func (m *mockTx) Rollback() 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 *sql.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
@@ -143,6 +144,24 @@ func TestUnitOfWork_InjectsExecutor(t *testing.T) {
} }
} }
// --- BeginTx / Stats ---
func TestComponent_BeginTx_NilDB(t *testing.T) {
c := &mysqlComponent{logger: newLogger()}
_, err := c.BeginTx(context.Background(), nil)
if err == nil {
t.Error("expected error for nil db")
}
}
func TestComponent_Stats_NilDB(t *testing.T) {
c := &mysqlComponent{logger: newLogger()}
stats := c.Stats()
if stats.MaxOpenConnections != 0 || stats.OpenConnections != 0 {
t.Errorf("expected zero DBStats, got %+v", stats)
}
}
// --- helpers --- // --- helpers ---
func assertCode(t *testing.T, err error, want xerrors.Code) { func assertCode(t *testing.T, err error, want xerrors.Code) {