# 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`, `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 `_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. ("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.