Skip to content

Struct Mapping

Ahmad Faiz Kamaludin edited this page Oct 18, 2026 · 2 revisions

One set of db tags drives both reading (scanning rows into structs) and writing (Rows, SetRow).

On this page: Tags · Writing structs · Rules for writes · The omitempty trap · Returning rows · db.Row

Tags

Tag Effect
db:"name" The column name. Default: snake_case of the field name
db:"name,omitempty" Left out of writes while the field holds its zero value
db:",omitempty" Default name, omitempty
db:"-" Never read or written
type User struct {
    ID        int64      `db:"id,omitempty"` // skipped on insert while 0, so the database assigns it
    Name      string     `db:"name"`
    Email     *string    `db:"email"`        // nil is written as NULL
    CreatedAt time.Time  `db:",omitempty"`   // column "created_at"
    Secret    string     `db:"-"`            // ignored entirely
}

Writing structs

id, err := conn.Insert("users").Rows(User{Name: "Ann"}).ExecGetID(ctx)
n, err  := conn.Insert("users").Rows(u1, u2).Exec(ctx)                 // or Rows([]User{u1, u2})
n, err   = conn.Insert("users").Rows(u1).Ignore().Exec(ctx)
n, err   = conn.Insert("users").Rows(u1).Upsert([]string{"email"}, []string{"name"}).Exec(ctx)
n, err   = conn.Update("users").SetRow(User{Name: "Anna"}).Where("id", id).Exec(ctx)
n, err   = conn.Update("users").SetMap(map[string]any{"email": nil}).Where("id", id).Exec(ctx) // explicit NULL

Take the table name from the type when you do not want to repeat it: name, err := db.TableName[User]().

A pointer to a struct also works: Rows(&u), SetRow(&u).

Rules for writes

  • Pointers and sql.Null*: a nil pointer is written as NULL. sql.Null* types and any driver.Valuer work as usual. A db.Raw(...) field value is inlined.
  • Batches: every row of a batch insert must have the same columns, so an omitempty column is left out only when it is zero in every row of one Rows call. If any row sets it, all rows write it.
  • Embedded structs are flattened. A field behind a nil embedded pointer counts as omitted (or NULL in a batch where another row sets it).
  • Unexported fields are ignored.
  • Update(...).SetRow(...) refuses to run without a WHERE (ErrNoWhere) and returns ErrNoValues when every field was omitted. See the WHERE guard.

The omitempty trap

Zero values with omitempty are skipped, so the database default or the existing value stays.

This is a common surprise in updates:

// Active is a bool tagged `db:"active,omitempty"`
conn.Update("users").SetRow(User{Active: false}).Where("id", 1).Exec(ctx)
// "active" is NOT written, because false is the zero value

To write a zero value:

  • use a pointer field (Age *int with &zero), or
  • use Set / SetMap: Set("active", false).

Returning rows

On PostgreSQL and SQLite, scan RETURNING rows into structs with the write builder's Get:

var rows []User
err := conn.Insert("users").Rows(User{Name: "Rae"}).Returning("*").Get(ctx, &rows)
err  = conn.Update("users").SetRow(User{Name: "Rae2"}).Where("id", 7).Returning("*").Get(ctx, &rows)
err  = conn.Delete("users").Where("id", 7).Returning("*").Get(ctx, &rows)

Returning is required before Get. MySQL and MariaDB return ErrUnsupported.

db.Row

db.Row(v) turns a struct into the map[string]any form that Values, SetMap and UpdateOrInsert take, applying the tag rules above. (Rows and SetRow do this for you.)

row, err := db.Row(User{Name: "Ann"})   // map[string]any{"name": "Ann", "email": (*string)(nil)}
sql, args, err := db.Use(db.Postgres).Insert("users").Values(row).ToSQL()

Row accepts a struct or non-nil pointer. Anything else (a map, nil, a scalar) is ErrInvalidArgs.

Clone this wiki locally