72 lines
4.0 KiB
Markdown
72 lines
4.0 KiB
Markdown
|
|
# 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.
|