Files
mysql/CLAUDE.md

75 lines
4.9 KiB
Markdown
Raw Permalink Normal View History

# 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`.
- **Configurable DSN parameters (v0.9.1)**: `Config.Charset`, `Config.Loc`, and `Config.ParseTime` are optional string fields that control the corresponding DSN parameters. Empty value means "use the safe default" (`utf8mb4`, `UTC`, `true`). Existing `Config` literals that do not set these fields behave identically to v0.9.0.
- **Collation DSN limitation**: `go-sql-driver` v1.8.x negotiates the connection collation via a 1-byte handshake ID (max 255). MariaDB 11.4+ collations such as `utf8mb4_uca1400_as_cs` exceed that range and cannot be specified in the DSN. Set collation at the schema level (database/table DDL) instead.
## 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.
- Do not pass a MariaDB 11.4+ collation (e.g. `utf8mb4_uca1400_as_cs`) as `Config.Collation` or any DSN parameter — the driver will fail at connect time with "unknown collation". Set collation in schema migrations at the database/table level instead.
## 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.