Two-field DB config: Driver + DSN, not a fused URL
byob-storage.2
configstorage
Problem: a single fused URL like DATABASE_URL=sqlite:///path.db or
postgres://user:pass@host/db seems tidy but re-invents URL parsing
in user code. sqlite file paths have hairy edge cases (:memory:,
file:...?mode=memory&cache=shared, relative vs absolute). Postgres
DSNs can be URL-form or libpq keyword-form. MySQL uses its own
user:pass@tcp(...) format. Scheme-dispatch code drifts into a full
parser as users hit edge cases.
Idea: match stdlib sql.Open(driver, dsn) directly. Config exposes
two fields — Driver string and DSN string — populated from env
vars (DB_DRIVER, DB_DSN) or TOML keys. The storage factory reads
them, calls sql.Open, and dispatches to the per-backend constructor.
Each driver parses its own DSN in whatever format it prefers; byob
code never touches string-level parsing.
Tradeoffs: users set two env vars / TOML keys instead of one URL. In
return: no scheme bikeshedding, no re-parsing edge cases, and the
factory code is five lines. If a user really wants URL convenience
later, a tiny ParseURL(s) (driver, dsn string, error) helper can be
added on top without changing the core config shape.
Design
// internal/config/config.go
type DB struct {
Driver string `toml:"driver"` // "sqlite" | "pgx"
DSN string `toml:"dsn"` // driver-specific
}
// internal/storage/open.go
type Store interface { /* ... consumer-narrow methods ... */ }
func Open(ctx context.Context, cfg config.DB) (Store, error) {
switch cfg.Driver {
case "sqlite":
return sqlite.Open(ctx, cfg.DSN)
case "pgx":
return postgres.Open(ctx, cfg.DSN)
default:
return nil, fmt.Errorf("unknown db driver: %q", cfg.Driver)
}
}
Sample DSNs:
# sqlite on disk
DB_DRIVER=sqlite
DB_DSN=file:state.db?_pragma=journal_mode(WAL)
# postgres
DB_DRIVER=pgx
DB_DSN=postgres://user:pass@host:5432/appdb?sslmode=require
# cockroachdb (postgres wire, different port + params)
DB_DRIVER=pgx
DB_DSN=postgres://user:pass@cockroach.host:26257/appdb?sslmode=require
Factory exposes the Store via a lazy sync.OnceValues closure (see
byob-factory-di.1) so mytool --help doesn't pay the cost of opening a
connection.