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/
2.7 KiB
ADR-001: database/sql Native Types
Status: Accepted Date: 2026-03-18
Context
Go's standard database/sql package was designed as a database-agnostic abstraction. Using it for MySQL is the conventional approach: the go-sql-driver/mysql package registers itself as a database/sql driver, and all query operations are expressed through *sql.DB, *sql.Tx, *sql.Rows, *sql.Row, and sql.Result.
An alternative would be to use a MySQL-specific client library analogous to pgx for PostgreSQL. However, there is no dominant MySQL-specific client with the same level of ecosystem adoption and capability that pgx has for PostgreSQL. Using database/sql with a standard driver is the practical and broadly understood choice.
The postgres module deliberately chose pgx native types. MySQL is the counterpart — it uses database/sql native types. The two modules are not interchangeable, which is by design.
Decision
The mysql module's Executor interface uses database/sql types throughout:
type Executor interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
Method names follow the database/sql convention (ExecContext, QueryContext, QueryRowContext) rather than the pgx convention (Exec, Query, QueryRow). This matches the underlying *sql.DB and *sql.Tx method signatures exactly, meaning *sql.DB satisfies Executor structurally without any wrapping.
The mysqlComponent embeds *sql.DB and delegates directly. mysqlTx wraps *sql.Tx and delegates to it.
Consequences
- Positive: Familiarity —
database/sqlis Go's standard library API, known to most Go developers. - Positive:
*sql.DBand*sql.Txnatively satisfy theExecutorinterface, reducing the need for wrapper code. - Positive: Any
database/sql-compatible driver (MariaDB, TiDB, etc.) could be substituted by changing the driver registration without touching the interface. - Negative:
sql.Resultis less informative thanpgconn.CommandTag(no command string, onlyLastInsertIdandRowsAffected). Applications that need PostgreSQL-style command metadata must use thepostgresmodule. - Negative:
*sql.Rowsmust be closed after use (defer rows.Close()). Forgetting this leaks connections back to the pool. - Note: The method naming difference from
postgres(ExecContextvsExec) is intentional and honest — it matches the actual API of the underlying library rather than artificially unifying the two modules.