Files
mysql/docs/adr/ADR-001-database-sql-native-types.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

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/sql is Go's standard library API, known to most Go developers.
  • Positive: *sql.DB and *sql.Tx natively satisfy the Executor interface, 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.Result is less informative than pgconn.CommandTag (no command string, only LastInsertId and RowsAffected). Applications that need PostgreSQL-style command metadata must use the postgres module.
  • Negative: *sql.Rows must be closed after use (defer rows.Close()). Forgetting this leaks connections back to the pool.
  • Note: The method naming difference from postgres (ExecContext vs Exec) is intentional and honest — it matches the actual API of the underlying library rather than artificially unifying the two modules.