Skip to content
Field Descriptors

Field Descriptors

What they are

A descriptor is a typed handle on a column. It knows both the column name and the Go type, so a mistyped column or a mismatched value is a compile error rather than a runtime surprise:

db.Model[User]().Where(UserFields.Active.Eq(true))   // checked
db.Model[User]().Where(orm.Eq("actve", true))        // compiles, fails at runtime

Descriptors are entirely optional. Everything they express can be written with the string constructors, and both produce identical SQL.

Generating them

lemmego orm:fields ./internal/models

Or, without the framework:

go run github.com/lemmego/orm/cmd/ormgen ./internal/models

Most often from a directive in the models package:

//go:generate go run github.com/lemmego/orm/cmd/ormgen .
FlagMeaning
-outoutput file name (default orm_fields.go)
-typecomma-separated model names to generate for
-allevery exported struct, not just tagged models
-stdoutwrite to standard output

By default it generates for exported structs carrying an orm or db tag, so DTOs and helper structs are left alone.

What it produces

For a model like:

type User struct {
    ID     uint64 `orm:"primaryKey;autoIncrement"`
    Email  string `orm:"column:email_address"`
    Active bool
    Posts  []Post `orm:"hasMany:UserID"`
}

it writes:

// Code generated by ormgen. DO NOT EDIT.

package models

import "github.com/lemmego/orm"

// UserTable is the conventional table name for User.
const UserTable = "users"

// UserFields holds typed column descriptors for User.
var UserFields = struct {
    ID     orm.Field[User, uint64]
    Email  orm.Field[User, string]
    Active orm.Field[User, bool]
}{
    ID:     orm.NewField[User, uint64]("id"),
    Email:  orm.NewField[User, string]("email_address"),
    Active: orm.NewField[User, bool]("active"),
}

Note what is absent: Posts is a relationship, not a column, so it gets no descriptor.

The generator handles embedded structs by inlining their columns, honours orm:"-", falls back to db tags, dereferences pointers for the value type, and reads a literal TableName() override off the source.

How column names are resolved

The generator calls the ORM’s own naming and tag rules rather than reimplementing them, so a descriptor always resolves to the same column the runtime queries. This is why descriptors are generated rather than written by hand: a second copy of the naming rules can disagree with the first, and the symptom is a query against a table that does not exist.

Available operators

UserFields.Age.Eq(30)        UserFields.Age.Ne(30)
UserFields.Age.Gt(18)        UserFields.Age.Gte(18)
UserFields.Age.Lt(65)        UserFields.Age.Lte(65)
UserFields.Age.In(18, 21)    UserFields.Age.NotIn(13, 14)
UserFields.Age.Between(18, 65)
UserFields.Email.IsNull()    UserFields.Email.IsNotNull()

UserFields.CreatedAt.Asc()   UserFields.CreatedAt.Desc()
UserFields.Email.Column()    // the underlying column name

Writing them by hand

Descriptors are ordinary Go values. Nothing stops you writing them yourself, which is worth doing for one or two models before committing to a generation step:

var UserFields = struct {
    Active orm.Field[User, bool]
}{
    Active: orm.NewField[User, bool]("active"),
}