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

6.8 KiB

name description
golang-bob 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):

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):

// 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 jetsmodels.Jets):

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.