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>
9.1 KiB
Bob Code Generation — bobgen (Layers 2–4)
bobgen introspects a live database (or .sql schema files) and generates: typed models +
setters + slices, a factory package for tests, typed query functions from your .sql files,
enums, and helper namespaces (SelectWhere, SelectJoins, <T>Columns, error constants). All
generated files end in .bob.go and are safe to regenerate. Bob does not manage migrations —
the schema must already exist.
The generator binaries
Four drivers under github.com/stephenafamo/bob/gen/ (no atlas/prisma driver in v0.46.0). Run with
go run ...@latest or go install. Each reads <DRIVER>_DSN from the env, or a config file via -c.
# PostgreSQL
PSQL_DSN='postgres://user:pass@host:5432/db?sslmode=disable' \
go run github.com/stephenafamo/bob/gen/bobgen-psql@latest
go run github.com/stephenafamo/bob/gen/bobgen-psql@latest -c ./bobgen.yaml
# MySQL
MYSQL_DSN='user:pass@tcp(host:3306)/db' go run github.com/stephenafamo/bob/gen/bobgen-mysql@latest
# SQLite
SQLITE_DSN='test.db' go run github.com/stephenafamo/bob/gen/bobgen-sqlite@latest
# SQL schema files (no live DB) — dialect is REQUIRED
SQL_DIALECT=psql go run github.com/stephenafamo/bob/gen/bobgen-sql@latest
Default config path is ./bobgen.yaml. Flag: -c FILE / --config FILE. A typical project wires
this into go:generate or a Makefile target.
Pin the generator to your runtime Bob version.
@latestdrifts; the generated code targets thegithub.com/stephenafamo/bobAPI and a generator newer/older than the version in yourgo.modcan emit code that doesn't compile. Use@v0.46.0(or whatever yourgo.modpins).
Configuration (bobgen.yaml)
Driver-specific keys are nested under the driver name; general keys sit at the top level.
psql: # driver block (mysql:/sqlite:/sql: for others)
dsn: "postgres://user:pass@host:5432/db?sslmode=disable"
driver: "github.com/jackc/pgx/v5" # default: github.com/lib/pq
schemas: ["public"]
shared_schema: "public" # this schema is omitted from generated names
uuid_pkg: "gofrs" # "gofrs" | "google"
queries: ["./queries"] # folders of .sql files -> typed query funcs (Layer 4)
concurrency: 10
column_order: "ordinal" # "ordinal" (DB order) | "name" (alphabetical)
only: # allow-list; value = optional column subset
"/^public\\./": # keys can be regexes (case-insensitive)
except: # deny-list
public.migrations:
public.addresses: [ updated_at ] # drop just these columns
"*": [ secret_col ] # from every table
# ---- general (top-level) ----
type_system: "github.com/aarondl/opt" # default; or "database/sql"
struct_tag_casing: "snake" # snake | camel | title
tags: [] # extra struct tags to emit
relation_loaded_name: "Loaded" # name of model.R.<this> ("Loaded" is reserved)
enum_format: "title_case" # title_case | screaming_snake_case
no_tests: false
# ---- which plugins run (each writes one package) ----
plugins_preset: "all" # all | none
plugins:
models: { pkgname: models, destination: models }
factory: { pkgname: factory, destination: factory }
enums: { pkgname: enums, destination: enums }
dbinfo: { disabled: false }
dberrors: { disabled: false }
where: { disabled: false }
joins: { disabled: false }
loaders: { disabled: false }
counts: { disabled: false }
Notes:
type_system: github.com/aarondl/opt(default) →null.Val[T]for nullable row fields andomit.Val[T]/omitnull.Val[T]in setters.database/sql→sql.Null[T]and pointers.plugins_preset: none+ selectively enabling plugins generates only what you need.- Setting a plugin
disabled: truedeletes the.bob.gofiles in its destination.
Per-driver extras: SQLite adds driver (modernc.org/sqlite or github.com/mattn/go-sqlite3)
and attach: { name: path.db }. The sql driver needs dialect: (psql/mysql/sqlite) and a
pattern: glob for the schema files (default *.sql).
Other config sections (see the configuration doc when needed): aliases (rename tables/columns/
relationships), constraints (declare PK/unique/FK that aren't in the DB), relationships (manual
relations — below), types (custom Go types + random/compare expressions for factories),
replacements (swap a column's generated type), inflections (pluralization overrides).
Typed queries from SQL (Layer 4 — the sqlc analog)
Point a driver's queries: key at folders of .sql files. Each file foo.sql generates
foo.bob.go (+ a test file). Name each query with a leading comment:
-- AllUsers
SELECT * FROM users WHERE id = ?;
SELECT * is expanded to explicit columns at generation time, so a schema change won't silently
break the result struct. The generated function takes typed params and returns a query you finish
the usual way:
row, err := AllUsers(1).One(ctx, db) // -> AllUsersRow
rows, err := AllUsers(1).All(ctx, db) // -> []AllUsersRow
// Add more mods without re-wrapping:
rows, err = AllUsers(1).With(sm.Where(psql.Quote("name").EQ(psql.Arg("Bob"))), sm.Limit(10)).All(ctx, db)
With() semantics: Where ANDs, OrderBy appends; Limit/Offset replace (psql/sqlite) but
append on MySQL — on MySQL only add them if the base query lacks them. For combined
UNION/INTERSECT queries use sm.OrderCombined/sm.LimitCombined.
Annotations override inferred names/types/nullability. On the query line:
-- Name *OneType:AllType:Transformer. Inline per column/param: /* name:type:notnull */ (any part
omittable, e.g. /* username */, /* ::notnull */, /* :big.Int: */).
Nested results from joins via column naming + --prefix: comments:
related.col(dot) → a to-many slice in the result struct.related__col(double underscore) → a to-one pointer.
-- Nested
SELECT users.*,
--prefix:videos.
videos.*,
--prefix:videos.sponsor__
sponsors.*
FROM users
LEFT JOIN videos ON videos.user_id = users.id
INNER JOIN sponsors ON videos.sponsor_id = sponsors.id;
-- -> NestedRow_{ ...users; Videos []NestedRow_Videos{ ...; Sponsor *NestedRow_Videos_Sponsor } }
Factories (Layer 3 — for tests)
A factory package is generated next to models (disable with plugins.factory.disabled: true).
Factories build or insert rows and auto-create required (non-nullable FK) relations. Random
values come from github.com/jaswdr/faker.
f := factory.New()
// A template = a recipe for one row. Mods set columns / relations.
tmpl := f.NewJet(
factory.JetMods.Name("Concorde"), // set a column
factory.JetMods.RandomAirportID(nil), // randomize a column (nil = default faker)
factory.JetMods.WithNewPilot( // create + attach a related pilot
factory.PilotMods.RandomizeAllColumns(nil),
),
)
// Base mods apply to every template from this factory:
f.AddBaseJetMods(factory.JetMods.RandomID(nil))
// Build (no DB):
setter := tmpl.BuildSetter() // a *JetSetter (ignores relations)
jet := tmpl.Build() // a *Jet with R populated from the template
// Create (inserts; required relations auto-created):
jet, err := tmpl.Create(ctx, db)
jets, err := tmpl.CreateMany(ctx, db, 10)
jet := tmpl.MustCreate(ctx, db) // panics on err
jet = tmpl.CreateOrFail(t, db) // calls t.Fatal on err (also CreateManyOrFail)
To-many relationship mods come in With*/WithNew* (overwrite) and Add*/AddNew* (append)
forms, e.g. factory.PilotMods.WithNewJets(5, mods...). Mark a relation never_required: true in
config to stop factories auto-creating it even when the FK is non-nullable.
Relationships config (when there's no FK, or it's complex)
Relationships are auto-detected from foreign keys (multi-column supported). Declare extra ones under
relationships: keyed by the "from" table:
relationships:
users:
- name: "users_to_videos_through_teams" # has-many-through
sides:
- { from: users, to: teams, columns: [[team_id, id]] }
- { from: teams, to: videos, columns: [[id, team_id]] }
- name: "verified_members" # static-value filter
sides:
- from: teams
to: users
columns: [[id, team_id]]
to_where:
- { column: verified, sql_value: "true", go_value: "true" }
Enums
A DB enum becomes a typed string constant set in the enums package:
CREATE TYPE task_status AS ENUM('not_started','in_progress','completed');
type TaskStatus string
const (
TaskStatusNotStarted TaskStatus = "not_started" // title_case (default)
TaskStatusInProgress TaskStatus = "in_progress"
TaskStatusCompleted TaskStatus = "completed"
)
func AllTaskStatus() []TaskStatus { /* ... */ }
enum_format: screaming_snake_case yields TaskStatusNOT_STARTED etc. The enum type is used
directly as the model field type for compile-time safety.