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>
217 lines
9.1 KiB
Markdown
217 lines
9.1 KiB
Markdown
# 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`.
|
||
|
||
```sh
|
||
# 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.** `@latest` drifts; the generated code targets
|
||
> the `github.com/stephenafamo/bob` API and a generator newer/older than the version in your
|
||
> `go.mod` can emit code that doesn't compile. Use `@v0.46.0` (or whatever your `go.mod` pins).
|
||
|
||
## Configuration (`bobgen.yaml`)
|
||
|
||
Driver-specific keys are nested under the driver name; general keys sit at the top level.
|
||
|
||
```yaml
|
||
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 and
|
||
`omit.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: true` **deletes** the `.bob.go` files 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:
|
||
|
||
```sql
|
||
-- 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:
|
||
|
||
```go
|
||
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.
|
||
|
||
```sql
|
||
-- 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`.
|
||
|
||
```go
|
||
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:
|
||
|
||
```yaml
|
||
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:
|
||
|
||
```sql
|
||
CREATE TYPE task_status AS ENUM('not_started','in_progress','completed');
|
||
```
|
||
```go
|
||
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.
|