Connect a database
Two modules do this job. gas/database owns the connection pool and hands out
gas.DatabaseProvider. gas/migrate applies schema changes that individual
services declare. They are separate because plenty of apps want a database
without migrations, and workers want migrations without a router.
go get github.com/gasmod/gas/database github.com/gasmod/gas/migrateWiring
Section titled “Wiring”app := gas.NewApp( gas.WithServiceInstance[gas.ConfigProvider](cfg), gas.WithSingletonService[gas.Logger](gaslog.NewSlogLogger()),
// Both are registered under their provider interface, because that is // what services (and migrate itself) inject. gas.WithSingletonService[gas.DatabaseProvider](database.New()), gas.WithSingletonService[gas.MigrationManager](migrate.New()),)Settings bind from the config provider, so a DSN can come from
DATABASE__DSN in the environment. To set them in code instead:
func explicitConfig() *database.Config { // Start from DefaultConfig so Mode and the pool settings are populated. A // bare &database.Config{} literal leaves Mode empty and fails Validate. cfg := database.DefaultConfig() cfg.Database.DSN = "postgres://user:pass@localhost:5432/mydb?sslmode=disable" cfg.Database.Driver = "pgx" return cfg}Choosing a mode
Section titled “Choosing a mode”| Mode | Backend | Use it when |
|---|---|---|
ModeSQL (default) |
database/sql |
Any driver: PostgreSQL, SQLite, MySQL |
ModePgx |
pgxpool.Pool |
PostgreSQL, and you want pgx types, batching, or the performance |
DB() returns a *sql.DB in both modes, so sqlc’s database/sql output always
works. In pgx mode, Pool() additionally returns the native pool for sqlc’s pgx
output, and database.PoolFrom(provider) unwraps it, reporting false when the
provider is not pgx-backed so you can fall back.
Transactions
Section titled “Transactions”WithTx commits when the function returns nil and rolls back otherwise,
including on panic.
// WithTx commits when fn returns nil and rolls back otherwise, including on// panic. A rollback that itself fails is joined onto the returned error.func createUser(ctx context.Context, db gas.DatabaseProvider) error { return db.WithTx(ctx, nil, func(tx *sql.Tx) error { if _, err := tx.ExecContext(ctx, "INSERT INTO users (email) VALUES ($1)", "a@example.com"); err != nil { return err } _, err := tx.ExecContext(ctx, "INSERT INTO profiles (user_id) VALUES (currval('users_id_seq'))") return err })}If the function returns an error, that error comes back and the rollback is
attempted; a rollback that itself fails is logged and joined onto it. A commit
failure is returned wrapped. In pgx mode, WithPgxTx mirrors this against
pgx.Tx, rolling back on a context detached from cancellation so cleanup still
runs when the caller’s context is already done.
Migrations belong to services
Section titled “Migrations belong to services”A service declares its own schema during Init. Nothing central lists every
migration, so deleting a service deletes its migrations with it.
// Services register their own migrations during Init, so a service owns its// schema and migrate applies everything in global version order at startup.type Service struct { migrations gas.MigrationManager}
func (s *Service) Name() string { return "notes" }
func (s *Service) Init() error { s.migrations.Register(s.Name(), gas.Migration{ Version: "20250216001", Description: "create notes table", Up: "CREATE TABLE notes (id SERIAL PRIMARY KEY, body TEXT NOT NULL);", Down: "DROP TABLE notes;", }) return nil}
func (s *Service) Close() error { return nil }RegisterSlice takes many at once, and RegisterFS reads .up.sql/.down.sql
pairs out of an embed.FS using the naming convention
{version}_{description}.up.sql.
Run (or Worker.Start) applies everything pending in global version order
across all services, after services initialize and before traffic is accepted.
Each migration runs in its own transaction.
Roll back with Down(n), which reverses the last n applied migrations in
reverse version order.
sqlc generates a DBTX interface in its own output package, satisfied by
*sql.DB and *sql.Tx in database/sql mode and by *pgxpool.Pool and
pgx.Tx in pgx mode. Pass DB(), Pool(), or a transaction straight into the
generated constructor. This module deliberately declares no DBTX of its own.
Health and readiness
Section titled “Health and readiness”CheckHealth (liveness) fails only when the service is uninitialized or closed.
It does not ping, because both database/sql and pgxpool reconnect on their
own and a restart would not help a transient outage. CheckReady does ping, so
a failing dependency drains traffic off the instance instead of killing it.
Config
Section titled “Config”| Field | Default | Description |
|---|---|---|
Database.Mode |
sql |
sql or pgx |
Database.Driver |
postgres |
database/sql driver name, ModeSQL only |
Database.DSN |
Connection string, required unless a connector is supplied | |
Database.MaxOpenConns |
25 |
Maximum open connections |
Database.MaxIdleConns |
5 |
Maximum idle connections, ModeSQL only |
Database.ConnMaxLifetime |
30m |
Maximum connection reuse time |
Database.ConnMaxIdleTime |
5m |
Maximum connection idle time |
Database.ConnRetries |
0 |
Connection retry attempts |
Database.ConnRetryInterval |
2s |
Base retry interval, doubling each attempt |
For full control over connection setup, such as custom TLS or IAM auth tokens,
pass a driver.Connector with database.WithConnector. Driver and DSN are then
not required. Connectors are ModeSQL only.