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>
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 (bydbtag / 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.