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

127 lines
4.6 KiB
Markdown

# 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:
```go
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
```go
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]`).
```go
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 |
```go
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:
```go
// 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:
```go
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.