skills/golang-bob/SKILL.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
6.8 KiB
Markdown

---
name: golang-bob
description: Use when writing Go against the Bob SQL toolkit (github.com/stephenafamo/bob) — building dialect SQL with query mods (sm/im/um/dm), generated models and setters, bobgen code generation, test factories, typed queries from .sql files, or scanning rows with bob.One/All/Cursor. Covers PostgreSQL, MySQL, and SQLite.
---
# Bob — Go SQL Access Toolkit
## Overview
Bob (`github.com/stephenafamo/bob`) is a Go SQL toolkit by Stephen Afamo — the author of
SQLBoiler, who started Bob as "an experiment for how v5 of SQLBoiler could look." It is
**database-first** and spans a fluent query builder up to a fully generated, type-safe ORM
with test factories. This skill targets **v0.46.0**.
Three principles drive every design decision:
1. **Correctness** — each dialect is hand-crafted to match its SQL spec; you cannot build a
query the dialect doesn't support (it won't compile).
2. **Convenience, not magic** — no hidden abstraction; queries map to the SQL you expect.
3. **Cooperation** — built on `database/sql` and `github.com/stephenafamo/scan`, not around them.
**Progressive adoption is the whole point.** Use one layer or all four:
| Layer | What you get | Analogous to | Type-safe |
|-------|--------------|--------------|-----------|
| 1. Query builder | Fluent, dialect-specific SQL builder (no DB knowledge) | squirrel | No (string-based) |
| 2. Models + ORM gen | Generated structs, CRUD, typed WHERE/JOIN mods, relationships, eager loading | SQLBoiler | **Yes** |
| 3. Factory gen | Generated test fixtures that auto-create required relations | Ruby FactoryBot | Yes |
| 4. Query gen | Type-safe Go funcs from hand-written `.sql` files | sqlc | Yes |
Cutting across all layers, the **SQL executor** (`bob.One/All/Cursor/Each/Exec`) builds and runs
a query and scans rows into structs in one step.
All three of PostgreSQL, MySQL/MariaDB and SQLite support every layer.
## When to use Bob (and when not)
**Reach for Bob when** you want database-first type safety without writing scan boilerplate;
you need to build *arbitrary* dialect-specific SQL (window functions, CTEs, `DISTINCT ON`,
`FOR UPDATE SKIP LOCKED`, upserts) that a lowest-common-denominator builder can't express;
or you want generated factories for relationship-heavy tests.
**The query builder alone** (Layer 1) is a fine, dependency-light replacement for squirrel even
if you never generate models.
**Consider alternatives when:** you want code-first/migrations-owned schema (Ent, GORM); you
only ever write raw SQL and just need typed funcs (plain sqlc); or you don't want a DB to exist
before generating code (Bob introspects a live DB or `.sql` schema files — it does **not** manage
migrations). See `comparisons.md` for a faithful Bob-vs-{GORM,Ent,SQLBoiler,Jet} table.
**Deliberately NOT in Bob:** automatic `created_at`/`updated_at` timestamps and soft-deletes.
Do timestamps at the DB level (defaults/triggers); soft-deletes are left to you.
## Critical conventions (read before writing code)
These are the things that trip people up — verified against v0.46.0 source:
- **Queries are built from "query mods."** `psql.Select(sm.From("users"), sm.Where(...))`
every argument is a mod. Each query type in each dialect has its own mod package, so wrong
mods fail at compile time.
- **Import paths are flat per dialect:** `dialect/psql`, `dialect/psql/sm`, `.../im`, `.../um`,
`.../dm`. Older docs/examples sometimes show `dialect/psql/insert/im` — that nesting does **not**
exist. (`sm`=select, `im`=insert, `um`=update, `dm`=delete mods; also `fm` function, `wm` window,
`vm` view, `mm` merge.)
- **`Arg()` makes a placeholder; a bare string is literal SQL.** `psql.Quote("id").EQ(psql.Arg(5))`
parameterizes `5`. Passing a Go value directly as a literal interpolates it as text — only `Arg`
(or `ArgGroup`) produces a bound parameter. This is the #1 injection footgun.
- **Bare strings in `any`-typed mod args are emitted verbatim, NOT quoted.** `sm.From("users")`
`FROM users` (unquoted). For a quoted identifier pass `sm.From(psql.Quote("users"))`. Some
string params (CTE names, `SetCol`, join `Using`) auto-quote; see `query-builder.md`.
- **`Raw` uses `?` placeholders in every dialect.** `psql.Raw("id = ?", 5)` — Bob rewrites `?`
to `$1`/`?1`/`?` per dialect on build. Escape a literal `?` as `\?`.
- **`.Apply()` mutates the query in place.** To reuse a base query, `.Clone()` first.
- **Hooks register with `AppendHooks`, not `Add`.** The hooks doc page shows `.Add(...)`; the real
method is `table.BeforeInsertHooks.AppendHooks(fn)`. Skip hooks per-call with `bob.SkipHooks(ctx)`.
## Quick reference
**Dialect + mod packages** (swap `psql` for `mysql`/`sqlite`):
```go
import (
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/psql" // Select/Insert/Update/Delete + starters (Arg, Quote, F, S, And...)
"github.com/stephenafamo/bob/dialect/psql/sm" // SELECT mods (From, Where, Join, GroupBy, Limit...)
"github.com/stephenafamo/bob/dialect/psql/im" // INSERT mods (Into, Values, OnConflict...)
"github.com/stephenafamo/bob/dialect/psql/um" // UPDATE mods (Table, SetCol, Where...)
"github.com/stephenafamo/bob/dialect/psql/dm" // DELETE mods (From, Where, Using...)
)
```
**Build → run** (two styles):
```go
// A) Build the string yourself, run with database/sql
q, args, err := psql.Select(sm.From("users"), sm.Where(psql.Quote("id").EQ(psql.Arg(1)))).Build(ctx)
rows, err := db.QueryContext(ctx, q, args...)
// B) Build + run + scan in one step with the executor
exec := bob.NewDB(sqlDB) // wrap *sql.DB
user, err := bob.One(ctx, exec, q, scan.StructMapper[User]()) // -> User
users, err := bob.All(ctx, exec, q, scan.StructMapper[User]()) // -> []User
```
**Generated-model usage** (after `bobgen`; table `jets``models.Jets`):
```go
jet, err := models.FindJet(ctx, db, 10) // by PK
jets, err := models.Jets.Query(models.SelectWhere.Jets.ID.EQ(100)).All(ctx, db)
jet, err = models.Jets.Insert(&models.JetSetter{Name: omit.From("x")}).One(ctx, db)
```
## Where to look next
Load the reference file for the layer you're working in — each has verbatim, dialect-correct examples:
| You are... | Read |
|------------|------|
| Building SQL by hand (any layer 1 work, mods, expressions, dialect differences) | `query-builder.md` |
| Using generated models — CRUD, setters, typed WHERE/JOIN, relationships, eager loading, hooks | `models.md` |
| Setting up `bobgen` — config, drivers, typed `.sql` queries, factories, enums, relationships config | `code-generation.md` |
| Running queries — executor, `One/All/Each/Cursor`, transactions, prepared statements, scanning | `execution.md` |
| Choosing Bob vs GORM/Ent/SQLBoiler/Jet | `comparisons.md` |
Hand-written ORM models (without codegen) and the `orm.NewTable`/`NewView` primitives are covered
at the end of `models.md`.