Skip to content
You are reading the v0.20 documentation. Go to the latest version (v1.0)

Database

Database

SQLite by default with safe pragmas and split reader/writer pools, transactions, and opt-in drivers for PostgreSQL, MySQL/MariaDB, Redis and MongoDB.

Last updated:

Support matrix#

DatabasePackageStatus
SQLite (pure Go, no cgo)core, modernc.org/sqliteAvailable
PostgreSQLdrivers/postgres (pgx)Available
MySQL 8 / MariaDB 11drivers/mysql (go-sql-driver/mysql)Available
Redis (cache, sessions, queues, rate limiting)drivers/redis (go-redis)Available
MongoDBdrivers/mongo (official driver)Available

Drivers are separate Go modules, so importing the core never downloads a heavy driver.

Connecting#

app.DB() opens the default connection on first use from DB_* variables, registers a db health check (/up?checks=1) and closes it on shutdown:

VariableDefault
DATABASE_URLfile:storage/database.sqlite
DB_MAX_OPEN10
DB_MAX_IDLE5
routes/web.go
db, err := app.DB() // *database.DB
if err != nil {
	return err
}
r.Resource("posts", controllers.NewPostController(db))

SQLite URLs: file:storage/app.db, sqlite:storage/app.db or :memory:. Other schemes need their driver, added with vukem.WithDatabaseDrivers(...).

Outside an app, database.Open(ctx, url, database.WithConfig(cfg)) returns the same *database.DB; database.FromSQL(conn, dialect) wraps an existing *sql.DB.

SQLite defaults#

Every SQLite connection gets WAL journaling, busy_timeout, foreign_keys=ON and BEGIN IMMEDIATE for write transactions. Writes and transactions go to a single-connection writer pool, reads to a read-only reader pool, so concurrent requests do not fail with SQLITE_BUSY.

Raw SQL#

*database.DB and *database.Tx implement database.Executor:

Go
var n int64
err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM posts WHERE published = ?", true).Scan(&n)

Always pass values as arguments; never concatenate input into SQL. db.Dialect() gives Quote and Placeholder for portable statements, and db.SQL() exposes the underlying *sql.DB.

Transactions#

Go
err := db.Tx(ctx, func(tx *database.Tx) error {
	if err := orm.Create(ctx, tx, &order); err != nil {
		return err
	}

	return orm.Create(ctx, tx, &item)
}) // commit on nil, rollback on error or panic (the panic is re-raised)

database.RunInTx(ctx, ex, fn) joins the caller's transaction when ex is already a *Tx, or starts one when it is a *DB, which keeps service functions composable.

Next#

  • ORM: typed queries and models.
  • Migrations: schema builder and vukem migrate.

PostgreSQL#

Terminal
go get github.com/AlexGalhardo/vukem/drivers/postgres
bootstrap/app.go
vukem.New(append([]vukem.Option{
	vukem.WithDatabaseDrivers(postgres.Driver()), // postgres:// and postgresql:// URLs
	// ...
}, opts...)...)
.env
DATABASE_URL=postgres://app:secret@localhost:5432/app?sslmode=disable

Models, queries, migrations, relations, the database cache store and SESSION_DRIVER=database work unchanged on SQLite, PostgreSQL and MySQL.

MySQL and MariaDB#

Terminal
go get github.com/AlexGalhardo/vukem/drivers/mysql
bootstrap/app.go
vukem.New(append([]vukem.Option{
	vukem.WithDatabaseDrivers(mysql.Driver()), // mysql:// and mariadb:// URLs
	// ...
}, opts...)...)
.env
DATABASE_URL=mysql://app:secret@localhost:3306/app

The URL becomes a go-sql-driver DSN with parseTime, UTC times, utf8mb4 and a 10s dial timeout; query parameters override them. Hosts other than localhost require TLS with a verified certificate unless the URL sets tls= (for example tls=preferred). Default MySQL collations compare text case-insensitively, so Where and unique indexes on strings ignore case.

Conformance suite#

database/dbtest.Run(t, db) is the suite every dialect passes: migrations, schema changes, ORM round trips (JSON, nullable, booleans, times), unique violations as 409, foreign keys, soft deletes, cursor pagination, relations and transactions (including rollback on panic). drivers/postgres runs it against postgres:17 and drivers/mysql against mysql:8.4 and mariadb:11.8 with go test -tags=integration.

Transactions and SQLite#

Inside db.Tx, pass tx to every query. SQLite has a single writer connection, so a write through db while the transaction holds it waits forever. database.RunInTx(ctx, ex, fn) joins the caller's transaction when ex is already a *database.Tx.