MySQL

Read a config or secret value from a MySQL / MariaDB table.

Schememysql://
Modulegithub.com/xavidop/mamori/providers/mysql
Sensitiveno (opt-in with WithSensitive)
Watchpoll
AuthDSN (WithDSN / MYSQL_DSN)

Install

go get github.com/xavidop/mamori/providers/mysql
import _ "github.com/xavidop/mamori/providers/mysql"

Using the ref

A mysql:// ref points at one row of a MySQL or MariaDB table (a key/value lookup), optionally selecting a field from a JSON value.

mysql://<table>/<key>[#json-field][?key_col=<c>&val_col=<c>]
PartRequiredWhat it means
<table>yesThe table to read. Validated against a strict identifier allowlist, then backtick-quoted.
<key>yesThe row key, bound as a ? placeholder (never interpolated) and matched with WHERE <key_col> = ?.
#json-fieldnoParse the value column as a JSON object and return one field (via mamori.SelectKey).
?key_col=<c>noOverride the key column name (default key).
?val_col=<c>noOverride the value column name (default value).

Examples

  • mysql://settings/greeting - runs SELECT value FROM settings WHERE key = ? with the key bound to greeting.
  • mysql://settings/workers?val_col=int_value - reads the same row but from the int_value column instead of value.
  • mysql://config/db#host - reads the JSON object at key db and returns its host field.
  • mysql://settings/feature_x?key_col=name&val_col=data - runs SELECT data FROM settings WHERE name = ?.
type Config struct {
	Greeting string `source:"mysql://settings/greeting"`
	Workers  int    `source:"mysql://settings/workers?val_col=int_value"`
}

The row key is always a bound ? placeholder; the table and column names are validated against a strict identifier allowlist (^[A-Za-z_][A-Za-z0-9_]*$) and backtick-quoted before any query runs, so a ref can never inject SQL. Value.Version is a content hash of the value by default, or a native revision column via WithVersionColumn. Values are non-sensitive unless you set WithSensitive(true) or wrap the field in secret.String.

Watch

MySQL has no built-in change notification, so mamori polls (WithPollInterval + jitter).

Configuration

import mysqlprov "github.com/xavidop/mamori/providers/mysql"

mamori.WithProvider(mysqlprov.New(mysqlprov.WithDSN("user:pass@tcp(mysql:3306)/appdb")))

Close() releases a pool this provider opened itself; a pool injected with WithDB is owned by the caller and stays open. Either way Close is terminal, not a no-op: after it returns, every Resolve on that provider fails with errors.Is(err, mamori.ErrUnavailable). A subsequent Load fails for the fields it feeds, and under Watch the default onfail policy (keeplast) freezes them at their last known-good value and reports the error to your error handler.

A default: tag does not cover this since it only applies to genuine absence (ErrNotFound), never to an error; falling back on error requires the explicit per-field opt-in onfail:"default".

Error classification

Beyond the not-found case, query failures are classified by the driver’s numeric server error code so mamori.ErrorKind can distinguish them:

Numbermamori kind
1044 (ER_DBACCESS_DENIED_ERROR), 1142 (ER_TABLEACCESS_DENIED_ERROR)permission_denied
1045 (ER_ACCESS_DENIED_ERROR)unauthenticated
1040 (ER_CON_COUNT_ERROR), 1203 (ER_TOO_MANY_USER_CONNECTIONS)unavailable
anything elseunknown

MySQL has no rate-limit error class, so nothing maps to rate_limited, and the syntax-error code is unreachable through this provider’s fixed query template, so nothing maps to invalid either. Numbers not listed above report unknown rather than being guessed at, and the original *mysqldriver.MySQLError stays reachable with errors.As.

A refused TCP connection (client errors 2002/2003, a fully down database) is not a *MySQLError - it is a net-level error whose exact wrapped type is not stable enough to match reliably, so it deliberately reports unknown rather than being type-guessed. A connection-limit rejection (1040/1203, a reachable but overloaded database) still reports unavailable.

Verified with an in-memory fake (including an identifier-allowlist rejection test); live behavior is covered by //go:build integration tests.