Migrating from go-pg

Bun is a rewrite of go-pgopen in new window that works with PostgreSQL, MySQL, and SQLite. It consists of:

  • Bun core that provides a query builder and models.
  • pgdriver package to connect to PostgreSQL.
  • migrate package to run migrations.
  • dbfixture to load initial data from YAML files.
  • Optional starter kit that provides modern app skeleton.

Bun's query builder tries to be compatible with go-pg's builder, but some rarely used APIs are removed (for example, WhereOrNotGroup). In most cases, you won't need to rewrite your queries.

go-pg is still maintained and there is no urgency in rewriting go-pg apps in Bun, but new projects should prefer Bun over go-pg. And once you are familiar with the updated API, you should be able to migrate a 80-100k lines go-pg app to Bun within a single day.

New features

  • *pg.Query is split into smaller structs, for example, bun.SelectQueryopen in new window, bun.InsertQueryopen in new window, bun.UpdateQueryopen in new window, bun.DeleteQueryopen in new window and so on. This is one of the reasons Bun inserts/updates data faster than go-pg.

    go-pg API:

    err := db.ModelContext(ctx, &users).Select()
    err := db.ModelContext(ctx, &users).Select(&var1, &var2)
    res, err := db.ModelContext(ctx, &users).Insert()
    res, err := db.ModelContext(ctx, &user).WherePK().Update()
    res, err := db.ModelContext(ctx, &users).WherePK().Delete()

    Bun API:

    err := db.NewSelect().Model(&users).Scan(ctx)
    err := db.NewSelect().Model(&users).Scan(ctx, &var1, &var2)
    res, err := db.NewInsert().Model(&users).Exec(ctx)
    res, err := db.NewUpdate().Model(&users).WherePK().Exec(ctx)
    res, err := db.NewDelete().Model(&users).WherePK().Exec(ctx)
  • To create VALUES (1, 'one') statement, use db.NewValues(&rows).

  • Bulk UPDATE queries should be rewrited using CTE and VALUES statement:

        With("_data", db.NewValues(&rows)).
        Set("model.name = _data.name").
        Where("model.id = _data.id").

    Alternatively, you can use UpdateQuery.Bulk helper that does the same:

    err := db.NewUpdate().Model(&rows).Bulk().Exec(ctx)
  • To create an index, use db.NewCreateIndex().

  • To drop an index, use db.NewDropIndex().

  • To truncate a table, use db.NewTruncateTable().

  • To overwrite model table name, use q.Model((*MyModel)(nil)).ModelTableExpr("my_table_name").

  • To provide initial data, use fixtures.

Go zero values and NULL

Unlike go-pg, Bun does not marshal Go zero values as SQL NULLs by default. To get the old behavior, use nullzero tag option:

type User struct {
    Name string `bun:",nullzero"`

For time.Time fields you can use bun.NullTime:

type User struct {
    Name      string    `bun:",nullzero"`
    CreatedAt time.Time `bun:",notnull,default:current_timestamp"`
    UpdatedAt bun.NullTime

Other changes

  • Replace pg struct tags with bun, for example, bun:"my_column_name".
  • Replace rel:"has-one" with rel:"belongs-to" and rel:"belongs-to" with rel:"has-one". go-pg used wrong names for those relations.
  • Replace tableName struct{} `pg:"mytable`" with bun.BaseModel `bun:"mytable"`. This helps with linters that mark the field as unused.
  • To marshal Go zero values as NULLs, use bun:",nullzero" field tag. By default, Bun does not marshal Go zero values as NULL any more.
  • Replace pg.ErrNoRows with sql.ErrNoRows.
  • Replace db.WithParam with db.WithNamedArg.
  • Replace orm.RegisterTable with db.RegisterModel.
  • Replace pg.Safe with bun.Safe.
  • Replace pg.Ident with bun.Ident.
  • Replace pg.Array with pgdialect.Array.
  • Replace pg:",discard_unknown_columns" with db.WithDiscardUnknownColumns() option.
  • Replace q.OnConflict("DO NOTHING") with q.On("CONFLICT DO NOTHING").
  • Replace q.OnConflict("(column) DO UPDATE") with q.On("CONFLICT (column) DO UPDATE").
  • Replace ForEach with sql.Rows and db.ScanRow.
  • Replace WhereIn("foo IN (?)", slice) with Where("foo IN (?)", bun.In(slice)).
  • Replace db.RunInTransaction with db.RunInTx.
  • Replace db.SelectOrInsert with an upsert:
res, err := db.NewInsert().Model(&model).On("CONFLICT DO NOTHING").Exec(ctx)
res, err := db.NewInsert().Model(&model).On("CONFLICT DO UPDATE").Exec(ctx)
subq := db.NewSelect()
q := db.NewSelect().
	With("subq", subq).

Ignored columns

Unlike go-pg, Bun does not allow scanning into explicitly ignored fields. For example, the following code does not work:

type Model struct {
    Foo string `bun:"-"`

But you can fix it by adding scanonly option:

type Model struct {
    Foo string `bun:",scanonly"`


You have 2 options if you need pg.Listener:

Porting migrations

Bun supports migrations via bun/migrate package. Because it uses timestamp-based migration names, you need to rename your migration files, for example, 1_initial.up.sql should be renamed to 20210505110026_initial.up.sql.

After you are done porting migrations, you need to initialize Bun tables (use starter kit):

go run cmd/bun/main.go -env=dev db init

And probably mark existing migrations as completed:

go run cmd/bun/main.go -env=dev db mark_applied

You can check the status of migrations with:

go run cmd/bun/main.go -env=dev db status

Monitoring performance

Bun comes with a plugin that allows to monitor performance and pinpoint bottlenecks using OpenTelemetry Tracing and Metrics.