Files
mysql/CLAUDE.md
Rene Nochebuena 9d8762458c feat(config): expose charset, loc, and parseTime as configurable DSN parameters
- add Config.Charset (MYSQL_CHARSET, default "utf8mb4"): connection character set
  sent as SET NAMES during handshake; previously hardcoded
- add Config.Loc (MYSQL_LOC, default "UTC"): IANA timezone for time.Time ↔
  DATE/DATETIME conversion; previously hardcoded
- add Config.ParseTime (MYSQL_PARSE_TIME, default "true"): driver-level DATE/DATETIME
  → time.Time mapping; valid values "true"/"false"; previously hardcoded
- update DSN() to derive parameters from Config fields with empty-means-default
  semantics; existing Config literals produce identical DSN output (backward compatible)
- remove unused url.URL construction from DSN(); params now built directly via url.Values
- document collation DSN limitation in Config godoc, CLAUDE.md, RELEASE.md, CHANGELOG.md:
  go-sql-driver v1.8.x uses 1-byte handshake collation IDs (max 255); MariaDB 11.4+
  collations such as utf8mb4_uca1400_as_cs exceed that range — set collation at the
  database/table level in schema migrations instead
2026-03-20 14:12:24 -06:00

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

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