Files
mysql/CLAUDE.md
Rene Nochebuena d9d07bcb70 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/
2026-03-19 13:21:34 +00:00

4.0 KiB

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.ErrNoRowsErrNotFound.
  • 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:

db := mysql.New(logger, cfg)
lc.Append(db)
r.Get("/health", health.NewHandler(logger, db))

Unit of Work:

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:

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.