Skip to content

Typed API

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

The core API fills a destination you pass in: Get(ctx, &users). The typed API, db.Query[T], returns typed values and can find the table name from the struct itself.

It is select-only. Writes with structs go through the write builders (Insert(...).Rows(...), Update(...).SetRow(...)), see Struct Mapping.

On this page: Naming the table · From and As · Reading · The typed chain · Scalar helpers

Naming the table

Give the row type a TableName() method:

type User struct {
    ID    int64   `db:"id,omitempty"`
    Name  string  `db:"name"`
    Email *string `db:"email"`
}

func (User) TableName() string { return "users" }
  • A value or pointer receiver both work. The method is promoted through embedding (an outer type's own TableName wins).
  • It is called on a zero value, once per type, and the result is cached. A name that changes later is not picked up.
  • A method that panics on a zero value (for example by reading a nil embedded pointer) is reported as db.ErrInvalidArgs naming the type, instead of crashing your program.
  • A type with no TableName(), or one returning "", is db.ErrInvalidArgs. The table is never guessed.
  • db.TableName[User]() returns the name directly.
  • For per-tenant or per-period tables, give the name yourself with As.

From and As

Entry point Table comes from Use it for
db.From[T](conn) T.TableName() The common case
db.As[T](sel) The *db.SelectBuilder you pass Maps, scalars, ad-hoc or dynamic tables, a builder you already built
users, err := db.From[User](conn).Where("active", true).Get(ctx)             // []User

ids, err   := db.As[int64](conn.Select("id").From("users")).Get(ctx)         // []int64
rows, err  := db.As[map[string]any](conn.Select().From("users")).Get(ctx)    // []map[string]any

tenantUsers, err := db.As[User](conn.Select().From("tenant_" + tenantID + "_users")).Get(ctx) // dynamic table

When building a dynamic table name from input, allow-list it first. See Security.

A problem with T (for example, no TableName) does not break the chain. It is reported by the first terminal call, like any builder error.

Both work with db.Use(...) to build SQL only:

db.From[User](db.Use(db.Postgres)).Where("id", 7).Builder().ToSQL()

Reading

Query[T] returns T instead of filling a pointer.

Method Returns
Get(ctx) []T
First(ctx) T, or ErrNotFound
Find(ctx, id) T where id = ?
Sole(ctx) The only T. ErrNotFound or ErrMultipleRecords
Paginate(ctx, page, perPage) db.Paged[T]
SimplePaginate(ctx, page, perPage) db.SimplePaged[T]
CursorPaginate(ctx, perPage, cursor, opts...) db.CursorPaged[T]
Chunk(ctx, size, fn func([]T) (bool, error))
ChunkByID(ctx, size, fn, column...)
Each(ctx, fn func(T) error)
Stream(ctx) iter.Seq2[T, error]
Count, Exists, DoesntExist, Sum, Avg As on the select builder

The paged types embed the page description and add the rows:

page, err := db.From[User](conn).OrderBy("id").Paginate(ctx, 2, 15)
fmt.Println(page.Total, page.LastPage, page.From, page.To) // promoted from db.Page
for _, u := range page.Items {                              // Items is []User
    // use u
}
Type Embeds Adds
db.Paged[T] db.Page Items []T
db.SimplePaged[T] db.SimplePage (HasMore) Items []T
db.CursorPaged[T] db.CursorPage (HasMore, NextCursor) Items []T

Scanning rules are the same as in Reading Data.

The typed chain

Go methods cannot take type parameters, so the select builder's Get(ctx, &dest) cannot return []T. That is why Query[T] is a separate type. It mirrors the select builder's chain methods (sources, Select*, every Where*, joins, grouping, ordering, limit and offset, unions, CTEs, locks and index hints) and returns itself, so T survives the chain. The mirror is generated from the select builder, so the two always match.

Exceptions:

  • When and Unless receive func(*db.Query[T]).
  • WhereGroup / HavingGroup receive *db.WhereClause / *db.HavingClause.
  • Min, Max, Value, Pluck and Aggregate are not mirrored. Use db.ValueAs, db.PluckAs, or the SelectBuilder readers.

For anything else, Apply hands you the underlying select builder:

db.From[User](conn).
    Apply(func(b *db.SelectBuilder) {
        b.WhereDate("created_at", day).WhereJsonContains("meta->tags", "go")
    }).
    OrderBy("id").
    Get(ctx)

Builder() returns the underlying *db.SelectBuilder (the same one, not a copy).

Scalar helpers

For a single value or column, without a struct:

name, err := db.ValueAs[string](ctx, conn.Select().From("users").Where("id", 7), "name")
ids, err  := db.PluckAs[int64](ctx, conn.Select().From("users").OrderBy("id"), "id")

NULL comes back as the zero value of the type. db.As[int64](...) with a single-column query gives the same results through Query[T].

Clone this wiki locally