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/
4.0 KiB
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):
Executorusessql.Result,*sql.Rows, and*sql.Row. Method names followdatabase/sqlconvention:ExecContext,QueryContext,QueryRowContext. Thepostgresmodule uses pgx types; these two are intentionally incompatible. - No ctx on Tx.Commit/Rollback (ADR-002):
database/sqldoes not support per-call context onCommitorRollback. Themysql.Txinterface honestly omitsctxfrom these methods rather than accepting and ignoring it.UnitOfWork.Docallstx.Commit()andtx.Rollback()without context. - Driver import alias (ADR-003): The
go-sql-driver/mysqldriver package name collides with the package namemysql. Inerrors.goit is imported asmysqldrvto disambiguate. Inmysql.goit is imported with_for side-effect registration only. - Error mapping via MySQLError.Number:
HandleErrortype-asserts to*mysqldrv.MySQLErrorand 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
postgresmodule — active*sql.Txis stored underctxTxKey{}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 isdatabase/sql; the two are distinct. - Do not add a
ctxparameter toTx.Commit()orTx.Rollback().database/sqldoes not support it; accepting and silently ignoring a context would be misleading. - Do not import
github.com/go-sql-driver/mysqldirectly in application code to type-assert on*mysql.MySQLError. Usedb.HandleError(err)instead — it maps driver errors to portablexerrorscodes. - Do not match MySQL errors by message string. Use
HandleErrorwhich switches onmysqldrv.MySQLError.Number. - Do not add package-level
*sql.DBvariables.mysqlComponentis the unit of construction; use dependency injection. - Do not forget
defer rows.Close()afterQueryContext— unclosed*sql.Rowshold connections from the pool.
Testing Notes
compliance_test.goasserts at compile time thatmysql.New(...)satisfiesmysql.Component.- Integration tests (real queries, transaction rollback) require a live MySQL or MariaDB instance, typically provided by a CI service container.
HandleErrorcan be unit-tested by constructing*mysqldrv.MySQLError{Number: 1062}directly — no database connection needed.- Pool initialization happens in
OnInit, notNew. Mocking theClientinterface bypasses pool setup entirely, making unit tests straightforward.