skills/golang-bob/execution.md
Alex Dunmow 7bc233df1a feat(golang-bob): add Bob Go SQL toolkit reference skill
Reference skill for github.com/stephenafamo/bob (v0.46.0): query builder,
generated models/ORM, bobgen code generation, factories, and result scanning.
SKILL.md routes to per-subsystem reference files (query-builder, models,
code-generation, execution, comparisons). Symlinked into ~/.claude/skills and
~/.agents/skills (Codex).

Verified with a RED/GREEN subagent test and cross-checked against v0.46.0
source (flat mod import paths, AppendHooks, plural table var, WhereMod ops).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:51:55 +08:00

4.6 KiB

Bob SQL Executor — running queries & scanning (cross-cutting)

The executor builds a query, runs it, and scans rows into Go values in one step. It's built on github.com/stephenafamo/scan; the scanning functions (bob.One, bob.All, …) are thin wrappers over scan.One/scan.All/etc.

Getting an Executor

bob.Executor is scan.Queryer (a QueryContext that returns scan.Rows) plus ExecContext. A plain *sql.DB does not satisfy it directly (its QueryContext returns *sql.Rows), so wrap it:

import "github.com/stephenafamo/bob"

db := bob.NewDB(sqlDB)                 // wrap an existing *sql.DB  -> bob.DB (embeds *sql.DB)
db, err := bob.Open("pgx", dsn)        // or open directly (wraps sql.Open)
// bob.OpenDB(connector), bob.NewConn(*sql.Conn) also exist.

bob.Open/sql.Open need the database/sql driver registered by a blank import — same as plain database/sql. For Postgres that's _ "github.com/lib/pq" (driver name "postgres") or _ "github.com/jackc/pgx/v5/stdlib" (driver name "pgx"); for SQLite, _ "modernc.org/sqlite".

bob.DB embeds *sql.DB, so all standard methods remain. Pass db (or a bob.Tx) as the Executor to any scanning function. Returning scan.Rows (an interface) rather than *sql.Rows is what makes executors mockable in tests.

Transactions

tx, err := db.BeginTx(ctx, nil)        // bob.Tx (embeds *sql.Tx)
// ... use tx as the Executor ...
err = tx.Commit(ctx)                   // or tx.Rollback(ctx)

// Or the managed helper — commits on nil error, rolls back on error/panic:
err = db.RunInTx(ctx, nil, func(ctx context.Context, exec bob.Executor) error {
    _, err := bob.Exec(ctx, exec, psql.Insert(/* ... */))
    return err
})

Mappers

A scanning function needs a scan.Mapper[T] that maps result columns to T:

  • scan.StructMapper[T]() — maps columns to struct fields (by db tag / name). The default for rows.
  • Single-column results map to primitives directly (e.g. scan.SingleColumnMapper[int]).
import "github.com/stephenafamo/scan"

type User struct {
    ID   int    `db:"id"`
    Name string `db:"name"`
}
mapper := scan.StructMapper[User]()

Scanning functions (package bob)

All take (ctx, exec Executor, q Query, m scan.Mapper[T]) unless noted. q is any built query (Layer 1 builder, or models.Jets.Query(...) — generated queries also expose their own .One/.All).

Function Returns Use for
bob.Exec(ctx, exec, q) (sql.Result, error) INSERT/UPDATE/DELETE with no rows back (no mapper)
bob.One(..., m) (T, error) exactly one row (sql.ErrNoRows if none)
bob.All(..., m) ([]T, error) all rows into a slice
bob.Allx[Tr](..., m) (V, error) all rows into a custom slice type via a transformer
bob.Cursor(..., m) (scan.ICursor[T], error) stream row-by-row; Next()/Get()/Close()
bob.Each(..., m) range-over-func stream via a Go 1.23 for ... range iterator
q := psql.Select(sm.From("users"), sm.Where(psql.Quote("id").EQ(psql.Arg(1))))

user,  err := bob.One(ctx, db, q, scan.StructMapper[User]())
users, err := bob.All(ctx, db, q, scan.StructMapper[User]())

// Stream a large result set without loading it all:
for user, err := range bob.Each(ctx, db, q, scan.StructMapper[User]()) {
    if err != nil { /* ... */ }
    // use user
}

// Or a cursor:
c, err := bob.Cursor(ctx, db, q, scan.StructMapper[User]())
defer c.Close()
for c.Next() {
    user, err := c.Get()
    _ = user; _ = err
}

// Custom slice type:
type Users []User
us, err := bob.Allx[bob.SliceTransformer[User, Users]](ctx, db, q, scan.StructMapper[User]())

Prepared statements

bob.Prepare (exec-only) and bob.PrepareQuery (returns rows) build a reusable statement. Arg is the type of the bound argument bundle:

// Query statement:
stmt, err := bob.PrepareQuery(ctx, db, q, scan.StructMapper[User]())
defer stmt.Close()
users, err := stmt.All(ctx)            // also .One(ctx), .Cursor(ctx)

// Exec statement:
estmt, err := bob.Prepare(ctx, db, psql.Update(/* ... */))
_, err = estmt.Exec(ctx)

PrepareQueryx returns a custom slice type, mirroring Allx.

Two-step alternative (no executor)

If you'd rather run the query with plain database/sql, build the string yourself and execute it:

q, args, err := psql.Insert(im.Into("films"), im.Values(psql.Arg("UA502"))).Build(ctx)
_, err = sqlDB.ExecContext(ctx, q, args...)

This cooperates fully with anything that takes a query string + args (pgx, sqlx, your own pool wrapper) — you only need the executor when you want Bob to do the scanning.