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>
127 lines
4.6 KiB
Markdown
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.
|