Generate typed columns
Let anetos gen declare a typed column for every field of your models,
and a handle for every relation, so queries say
PostCols.Title.Like("%go%") and With(PostRels.Author), and the compiler
checks the column name, the value’s type and the relation’s model.
Before you start
Define your models. You need Go 1.26 or later.
Steps
1. Add the anetos tool to your module
go get -tool anetos.dev/anetos/cli/cmd/anetos@latestThis records the tool in go.mod (a tool line), so everyone on the
project runs the same version with go tool anetos. It adds nothing to
your application binary.
2. Generate
go tool anetos genFor every package with models under the current directory, this writes
models_gen.go next to them:
// illustrative (an excerpt of examples/database/models_gen.go)
// Code generated by anetos gen. DO NOT EDIT.
package main
import (
"time"
"anetos.dev/anetos/db"
)
// AuthorCols are the columns of [Author].
var AuthorCols = struct {
ID db.Column[int64]
CreatedAt db.Column[time.Time]
UpdatedAt db.Column[time.Time]
Name db.Column[string]
Email db.Column[string]
}{
ID: db.Col[int64]("id"),
CreatedAt: db.Col[time.Time]("created_at"),
UpdatedAt: db.Col[time.Time]("updated_at"),
Name: db.Col[string]("name"),
Email: db.Col[string]("email"),
}Each model Post gets PostCols (an unexported post gets postCols),
with one field per column, named like the struct field and typed like it.
Commit the file: it is plain Go, and go to definition works on it.
A model is a struct that:
- embeds
db.Model,db.Timestampsordb.SoftDeletes(directly, or through another embedded struct), or - has a
TableName() stringmethod, or - has a
//anetos:modelline in its doc comment.
Add //anetos:skip to leave one out, such as a base struct you only
embed:
// illustrative
// Base is embedded by the models of this package.
//
//anetos:skip
type Base struct {
db.Model
TenantID int64
}To regenerate with go generate ./..., add this line to one file of the
module, as examples/database does:
// illustrative
//go:generate go tool anetos genRegenerate after changing a model. go tool anetos dev does it for you
on every rebuild, and make:model right away.
3. Query with the columns
func (Blog) ListPosts(c *web.Ctx, in ListPosts) (db.Page[Post], error) {
q := db.Query[Post](c).Where(PostCols.PublishedAt.NotNull())
if in.Search != "" {
q = q.Where(PostCols.Title.Like("%" + in.Search + "%"))
}
if in.Author != nil {
q = q.Where(PostCols.AuthorID.Eq(*in.Author))
}
return q.With(PostRels.Author).Latest().Paginate(in.Page, in.PerPage) // one query for all the authors
}(Copied from examples/database, region list-posts.)
A column’s type is its field’s type, so:
Nullable columns are pointers: compare with
new(t), and setNULLwithnil:// illustrative recent := db.Query[Post](ctx).Where(PostCols.PublishedAt.Gt(new(since))) _, err := recent.Update(PostCols.PublishedAt.Set(nil)) // unpublishJSON columns (
db:"tags,json") encode the values you pass as JSON, anddb.Pluckdecodes them. Equality on JSON follows the database (PostgreSQL’sjsonbcompares documents, itsjsontype has no=, and MySQL and SQLite compare text), so preferdb.SQLwith the database’s JSON functions for conditions inside documents.Joins need qualified names:
PostCols.ID.Of("posts")isposts.id.
4. Check it in CI (optional)
go tool anetos gen -checkexits with status 1 and names the files that are out of date, without writing anything.
How it works
anetos gen loads your packages with the Go type checker (no code runs),
finds the models, and applies the same column rules as the db package
at runtime: db tags, snake_case names for untagged fields, embedded
structs flattened, rel fields as relations, other struct fields skipped. A test in the framework
compares the two on a fixture of every kind of field. Mistakes the runtime
would report on first use, such as two fields mapping to one column or an
unknown db or rel tag option, are reported by anetos gen instead.
Key columns of relations are checked when a relation is first used
(PostRels.Author.Err() in a test checks them early).
It only replaces files that start with its // Code generated by anetos gen. DO NOT EDIT. header, and removes models_gen.go from a package that
no longer has models. A stale models_gen.go, or code that uses a column
you haven’t generated yet, doesn’t stop it.
Coming from Laravel? Eloquent resolves attributes by string at runtime (
where('title', 'like', …)). Here the names are generated Go values, so a typo or a renamed column breaks the build, not a request. Strings still work where you want them:db.C("title").
Testing it
Nothing to test in your app: the generated code is declarations only. Run
go tool anetos gen -check in CI so a model change without regeneration
fails the build.
Common problems
| Symptom | Cause | Fix |
|---|---|---|
undefined: PostCols | Not generated yet, or Post isn’t detected as a model | Run go tool anetos gen; mark a plain struct //anetos:model |
PostCols is already declared | Your code declares that name | Rename it, or mark Post //anetos:skip |
columns "name" and "inner_name" both come from fields named Name | An embedded struct and the model have fields with the same name | Rename one field |
models_gen.go was not written by anetos gen | A file of yours has that name | Rename your file |
field types must compile | A model field’s type has an error | Fix the compile error first |
go: no such tool "anetos" | The tool isn’t in go.mod | Run step 1 |