feat(mysql): initial stable release v0.9.0

database/sql-backed MySQL client with launcher lifecycle, health check, unit-of-work via context injection, and structured error mapping.

What's included:
- Executor / Tx / Client / Component interfaces using database/sql native types (sql.Result, *sql.Rows, *sql.Row)
- Tx.Commit() / Tx.Rollback() without ctx, matching the honest database/sql contract
- New(logger, cfg) constructor; *sql.DB opened in OnInit
- Config struct with env-tag support for all pool tuning parameters
- UnitOfWork via context injection; GetExecutor(ctx) returns active *sql.Tx or *sql.DB
- HandleError mapping MySQLError.Number to xerrors codes (1062 → AlreadyExists, 1216/1217/1451/1452 → InvalidInput, ErrNoRows → NotFound)
- Driver imported as mysqldrv alias to avoid package name collision
- health.Checkable at LevelCritical; HealthCheck delegates to db.PingContext

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
This commit is contained in:
2026-03-19 13:21:34 +00:00
commit d9d07bcb70
16 changed files with 858 additions and 0 deletions

71
CLAUDE.md Normal file
View File

@@ -0,0 +1,71 @@
# mysql
database/sql-backed MySQL client with launcher lifecycle, health check integration, and unit-of-work transaction management.
## Purpose
Provides a `Component` that manages a `*sql.DB` connection pool, satisfies the `launcher.Component` lifecycle hooks (`OnInit`, `OnStart`, `OnStop`), and implements `health.Checkable` (priority: critical). Also provides `NewUnitOfWork` for wrapping multiple repository operations in a single transaction via context injection.
## Tier & Dependencies
**Tier 3** (infrastructure) — depends on:
- `code.nochebuena.dev/go/health` (Tier 1)
- `code.nochebuena.dev/go/launcher` (Tier 1)
- `code.nochebuena.dev/go/logz` (Tier 0)
- `code.nochebuena.dev/go/xerrors` (Tier 0)
- `github.com/go-sql-driver/mysql` (external driver)
## Key Design Decisions
- **database/sql native types** (ADR-001): `Executor` uses `sql.Result`, `*sql.Rows`, and `*sql.Row`. Method names follow `database/sql` convention: `ExecContext`, `QueryContext`, `QueryRowContext`. The `postgres` module uses pgx types; these two are intentionally incompatible.
- **No ctx on Tx.Commit/Rollback** (ADR-002): `database/sql` does not support per-call context on `Commit` or `Rollback`. The `mysql.Tx` interface honestly omits `ctx` from these methods rather than accepting and ignoring it. `UnitOfWork.Do` calls `tx.Commit()` and `tx.Rollback()` without context.
- **Driver import alias** (ADR-003): The `go-sql-driver/mysql` driver package name collides with the package name `mysql`. In `errors.go` it is imported as `mysqldrv` to disambiguate. In `mysql.go` it is imported with `_` for side-effect registration only.
- **Error mapping via MySQLError.Number**: `HandleError` type-asserts to `*mysqldrv.MySQLError` and switches on `.Number`. Error codes 1062 (duplicate key) → `ErrAlreadyExists`; 1216, 1217, 1451, 1452 (foreign key violations) → `ErrInvalidInput`. `sql.ErrNoRows``ErrNotFound`.
- **UnitOfWork via context injection**: Same pattern as the `postgres` module — active `*sql.Tx` is stored under `ctxTxKey{}` in the context. `GetExecutor(ctx)` returns the transaction if present, otherwise `*sql.DB`.
## Patterns
Lifecycle registration:
```go
db := mysql.New(logger, cfg)
lc.Append(db)
r.Get("/health", health.NewHandler(logger, db))
```
Unit of Work:
```go
uow := mysql.NewUnitOfWork(logger, db)
err := uow.Do(ctx, func(ctx context.Context) error {
exec := db.GetExecutor(ctx) // returns active *sql.Tx
_, err := exec.ExecContext(ctx, "INSERT INTO ...")
return err
})
```
Error handling in repository code:
```go
rows, err := db.GetExecutor(ctx).QueryContext(ctx, "SELECT ...")
if err != nil {
return db.HandleError(err)
}
defer rows.Close()
```
## What to Avoid
- Do not use pgx types (`pgx.Rows`, `pgconn.CommandTag`, etc.) in code that depends on this module. This is `database/sql`; the two are distinct.
- Do not add a `ctx` parameter to `Tx.Commit()` or `Tx.Rollback()`. `database/sql` does not support it; accepting and silently ignoring a context would be misleading.
- Do not import `github.com/go-sql-driver/mysql` directly in application code to type-assert on `*mysql.MySQLError`. Use `db.HandleError(err)` instead — it maps driver errors to portable `xerrors` codes.
- Do not match MySQL errors by message string. Use `HandleError` which switches on `mysqldrv.MySQLError.Number`.
- Do not add package-level `*sql.DB` variables. `mysqlComponent` is the unit of construction; use dependency injection.
- Do not forget `defer rows.Close()` after `QueryContext` — unclosed `*sql.Rows` hold connections from the pool.
## Testing Notes
- `compliance_test.go` asserts at compile time that `mysql.New(...)` satisfies `mysql.Component`.
- Integration tests (real queries, transaction rollback) require a live MySQL or MariaDB instance, typically provided by a CI service container.
- `HandleError` can be unit-tested by constructing `*mysqldrv.MySQLError{Number: 1062}` directly — no database connection needed.
- Pool initialization happens in `OnInit`, not `New`. Mocking the `Client` interface bypasses pool setup entirely, making unit tests straightforward.