Skip to content
Generate typed columns

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@latest

This 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 gen

For 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.Timestamps or db.SoftDeletes (directly, or through another embedded struct), or
  • has a TableName() string method, or
  • has a //anetos:model line 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 gen

Regenerate 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 set NULL with nil:

    // illustrative
    recent := db.Query[Post](ctx).Where(PostCols.PublishedAt.Gt(new(since)))
    _, err := recent.Update(PostCols.PublishedAt.Set(nil)) // unpublish
  • JSON columns (db:"tags,json") encode the values you pass as JSON, and db.Pluck decodes them. Equality on JSON follows the database (PostgreSQL’s jsonb compares documents, its json type has no =, and MySQL and SQLite compare text), so prefer db.SQL with the database’s JSON functions for conditions inside documents.

  • Joins need qualified names: PostCols.ID.Of("posts") is posts.id.

4. Check it in CI (optional)

go tool anetos gen -check

exits 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

SymptomCauseFix
undefined: PostColsNot generated yet, or Post isn’t detected as a modelRun go tool anetos gen; mark a plain struct //anetos:model
PostCols is already declaredYour code declares that nameRename it, or mark Post //anetos:skip
columns "name" and "inner_name" both come from fields named NameAn embedded struct and the model have fields with the same nameRename one field
models_gen.go was not written by anetos genA file of yours has that nameRename your file
field types must compileA model field’s type has an errorFix the compile error first
go: no such tool "anetos"The tool isn’t in go.modRun step 1

Next steps