App

PostgreSQL

Wire a GoLazy application to PostgreSQL-backed migrations, jobs, and storage packages.

By Guillermo Alvarez - Published - Updated

Start with init/db.go

PostgreSQL is application glue first. Create the pool once, pass it to lazyapp.Config, and put the reusable database helper in init/db.go.

// init/app.go
func App() *lazyapp.App {
    db, err := OpenDB(context.Background())
    if err != nil {
        panic(err)
    }

    return lazyapp.New(lazyapp.Config{
        Name:   "sample_app",
        Drawer: Draw,
        Public: app.Public,
        Views:  app.Views,
        Dependencies: func(deps *lazydeps.Scope) error {
            if err := Database(deps, db); err != nil {
                return err
            }
            return Dependencies(deps)
        },
        Jobs: lazyjobs.Config{
            Backend: pgjobs.New(db),
            Define:  jobs.DefinedJobs,
        },
    })
}
// init/db.go
package appinit

import (
    "context"

    "github.com/jackc/pgx/v5/pgxpool"
    "golazy.dev/lazydeps"
    "golazy.dev/pg"
)

type dbKey struct{}

func OpenDB(ctx context.Context) (*pgxpool.Pool, error) {
    return pg.OpenEnv(ctx, "DATABASE_URL")
}

func Database(deps *lazydeps.Scope, db *pgxpool.Pool) error {
    _, err := lazydeps.Service(deps, "postgres", func(ctx context.Context) (
        context.Context,
        *pgxpool.Pool,
        error,
        context.CancelFunc,
    ) {
        return context.WithValue(ctx, dbKey{}, db), db, nil, db.Close
    })
    return err
}

func DB(ctx context.Context) (*pgxpool.Pool, bool) {
    db, ok := ctx.Value(dbKey{}).(*pgxpool.Pool)
    return db, ok
}

The pool is app-owned. PostgreSQL service packages receive the pool, but the dependency graph closes it.

Package map

The core golazy.dev module keeps PostgreSQL out of its dependency graph. Concrete implementations live in golazy.dev/pg:

  • golazy.dev/pg: shared pgxpool helpers.
  • golazy.dev/pg/pgmigrate: PostgreSQL backend for golazy.dev/lazymigrate.
  • golazy.dev/pg/pgjobs: PostgreSQL backend for golazy.dev/lazyjobs.
  • golazy.dev/pg/withpg: embedded PostgreSQL helper for local integration tests.

Continue with Dependencies And Services for dependency ownership and Background Jobs for job registration.

Migrations

Application migrations live under migrations/<database>/, with a matching migrations/migrations.toml entry:

[postgres]
backend = "golazy.dev/pg/pgmigrate"
url_env = "DATABASE_URL"

The PostgreSQL backend owns the SQL format. Use lazy markers, not Goose markers:

-- +lazy Up
CREATE TABLE posts (
    id BIGSERIAL PRIMARY KEY,
    title TEXT NOT NULL
);

-- +lazy Down
DROP TABLE posts;

lazymigrate loads sortable migration files, asks the backend which migrations have already run, computes the diff, and calls the backend for each up or down step. pgmigrate creates the lazy_migrations metadata table, lists applied migration ids, runs the selected section in a transaction, and records the checksum.

Package backends can embed their own migrations. For example, pgjobs exposes the lazy job table migration alongside its lazyjobs.Backend.

Local tests

The golazy.dev/pg integration tests read GOLAZY_PG_DATABASE_URL. golazy.dev/pg/withpg starts embedded PostgreSQL and sets both DATABASE_URL and GOLAZY_PG_DATABASE_URL for the command it runs.

go run ./pg/withpg/cmd/withpg --version 16 --version 17 -- go test ./pg/...

Go tests can use withpg.Test to create one subtest per configured PostgreSQL version:

withpg.Test(t, withpg.Config{PgVersions: []string{"16", "17"}}, func(t *testing.T, db withpg.DB) {
    t.Parallel()
    // Use db.URL() or db.Client(t.Context()).
})

Generator direction

lazy generate pg will later modify an existing app. It should create or merge init/db.go, add the golazy.dev/pg module dependency, create migrations/migrations.toml, and wire selected PostgreSQL package backends through lazyapp.Config.