skills/golang-bob/code-generation.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

217 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Bob Code Generation — bobgen (Layers 24)
`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.