Skip to content

Fields ​

Introduction ​

A model's field says what its column is, what a row gets when the field is left empty, and what a write may put in it. You say all three with attributes after the field, so the table, the defaults and the write checks come from one place.

Declaring Fields ​

This model declares its columns, defaults and checks with attributes:

go
type Order struct {
	orm.Model[Order]
	ID     int64
	Ref    string
	Status string
	Qty    int
	Email  string
	Phone  Phone
	Placed time.Time
	Due    time.Time
}

func (o *Order) Schema(s *orm.Schema) {
	s.Field(&o.Ref, orm.Char{Max: 12, Min: 3, Pattern: `^[A-Z0-9-]+$`, Trim: true, Label: "Reference"})
	s.Field(&o.Status, orm.Char{Max: 20, Default: "draft", Choices: []string{"draft", "paid", "void"}})
	s.Field(&o.Qty, orm.Int{Min: 1, Max: 100, Size: orm.Small, Default: 1})
	s.Field(&o.Email, Email{Unique: true})
	s.Field(&o.Placed, orm.Datetime{DefaultNow: true, Precision: 3, UTC: true})
	s.Field(&o.Due, orm.DateField{Help: "the day payment is due"})
}

Status is a VARCHAR(20) with DEFAULT 'draft' in the table, an order created without one is a draft, and a write giving it "lost" is refused before it reaches the database. Email and Phone use an attribute and a type of the program's own (below).

Column Attributes ​

@orm.PK, @orm.Char, @orm.Int, @orm.DateField and @orm.Datetime each take these, all optional:

Field
Columnthe column's name; the field's in snake_case otherwise
Nullthe column takes NULL (a pointer or Option field does already)
Unique, Indexa unique constraint, an index
Defaultthe column default, a Go value (nil: none) — see Defaults
Commentthe column's comment in the table (Postgres, MySQL; SQLite keeps none)
Label, Helpwhat forms and tools show for the field (see Field info)

@orm.PK takes Column, Comment, Label, Help and Default: a key is never NULL, and is unique and indexed already. The bare forms, @orm.PK and @orm.Char{Max: 200}, are what they always were.

Attribute Settings ​

Each attribute also has fields of its own:

AttributeIts own fields
@orm.PKManual: an integer key the program sets, not one the database counts (0 is then a key like any). UUID: a string key the ORM sets to a random UUID on create when it is empty — uuid on Postgres, CHAR(36) on MySQL and SQLite
@orm.CharMax: a VARCHAR(Max), and the most characters a write may hold. Min: the fewest. Choices: the values it may take. Pattern: a regular expression (Go's syntax) it matches. Trim, Lower: its surrounding spaces removed, lower case, as it is written. Collation: the column's (utf8mb4_bin, "C", NOCASE)
@orm.IntMin, Max: bounds, an integer each, nil for none — Min: 0 is a bound. Size: orm.Small (SMALLINT), orm.Medium (INTEGER, 32 bits), orm.Big (BIGINT), over the Go type's. Unsigned: UNSIGNED on MySQL, and negative values refused on every database. Choices: the values it may take
@orm.DateFielda DATE column of a time.Time field: the day alone. AutoNow, AutoNowAdd: today on every save, on create. DefaultNow: today on create when it is empty, and DEFAULT CURRENT_DATE in the table
@orm.Datetimea timestamp of a time.Time field. AutoNow, AutoNowAdd, DefaultNow as above, with CURRENT_TIMESTAMP. Precision: its fractional digits, 1–6 (DATETIME(n) on MySQL, timestamp(n) on Postgres; 0 leaves the database's, microseconds) — times are cut to it as they are written. UTC: written and read in UTC

Combining Attributes ​

The date is @orm.DateField because orm.Date is the lookup transform.

The attributes of earlier versions stay as they were, and mix with these: @orm.Column, @orm.Null, @orm.Unique, @orm.Index, @orm.AutoNow, @orm.AutoNowAdd, @orm.DefaultTo, @orm.DefaultSQL, @orm.DBType (see Models). A field's attributes apply left to right, each over the ones before it, and a member left empty never undoes an earlier attribute: in @orm.Column("ref_no") @orm.Char{Max: 20} the column is ref_no; in @orm.Char{Column: "a"} @orm.Column("b") it is b.

Default Values ​

A Default (or @orm.DefaultTo(v)) is two things at once:

  • the column's DEFAULT in the table migrations make, so a row SQL inserts without the column takes it;
  • the value Create (and BulkCreate, GetOrCreate) gives the field when it is left at its zero value.

This fragment shows both at work:

go
o := Order{Ref: "A-1", Email: "ada@example.com", Phone: "+255 712 000"}
if err := o.Save(ctx); err != nil { // o.Status == "draft", o.Qty == 1, o.Placed is now
	return err
}

WARNING

The zero value is what counts as empty, so a bool with Default: true is always true on create: give it an Option[bool] (None is empty) to create it false.

@orm.DefaultSQL("…") is the table's default alone, an expression the ORM doesn't evaluate. DefaultNow is the database's current time in the table, and the program's clock (orm.Clock) on create.

Validation ​

A write checks each value against its field before it reaches the database: Create, BulkCreate (every row), GetOrCreate, Save, UpdateOrCreate and a query's Update (each value it sets that isn't an expression). What fails is an orm.Invalid error naming the columns, the shape a foreign key violation has. This fragment catches one:

go
err := orm.Objects[Order]().Create(ctx, &Order{Ref: "a", Status: "lost", Qty: 500})
fmt.Println(errors.Is(err, orm.ErrInvalid)) // true
var e *orm.Error
if errors.As(err, &e) {
	fmt.Println(e.Fields) // map[qty:[must be at most 100] ref:[must be at least 3 characters must match ^[A-Z0-9-]+$] status:[must be one of draft, paid, void] …]
}
CheckMessage
Char's Max (and a tag's size)must be at most n characters
Char's Minmust be at least n characters
Patternmust match <pattern>
Choicesmust be one of a, b, c
Int's Min, Max; Unsignedmust be at least n, must be at most n
the type's Validate(), an attribute's Validate(v)the error's text

NOTE

A NULL (a nil pointer, None) passes every check.

Trim and Lower run first, so " Ada@X.org " is checked, and stored, as "ada@x.org"; the row you passed holds what was written, the stamps of a UTC or Precision field included. A web app answers Invalid with a 422 and the fields' messages.

Custom Value Types ​

A field's type can say how its values reach the database and come back, with any of four methods — each optional, each independent of the others:

go
type Phone string

// DBValue is what's written, and what conditions compare the column with.
func (p Phone) DBValue(d orm.Dialect) (any, error) {
	return strings.ReplaceAll(string(p), " ", ""), nil
}

// FromDB reads the column's value, as the driver gives it.
func (p *Phone) FromDB(src any) error {
	*p = Phone(fmt.Sprint(src))
	return nil
}

// ColumnType is the column's type in migrations.
func (Phone) ColumnType(d orm.Dialect) string { return "VARCHAR(20)" }

// Validate checks each value a write gives the field.
func (p Phone) Validate() error {
	if !strings.HasPrefix(string(p), "+") {
		return errors.New("must start with a country code")
	}
	return nil
}
Method
DBValue(d orm.Dialect) (any, error)the value written on Create, Save and Update, and the value lookups compare: Order.Phone.Eq("+255 712 000") finds +255712000
FromDB(src any) error (pointer receiver)the value read; src is nil for NULL unless the field is a pointer or an Option, which are nil and None then
ColumnType(d orm.Dialect) stringthe column's type on each dialect, "" for the ORM's choice; @orm.DBType on the field wins over it
Validate() errora write check, as above

d.Name() is "postgres", "mysql" or "sqlite". A type may also be a sql.Scanner and driver.Valuer: then the ORM uses DBValue and FromDB for the model's column, and database/sql's methods stay for code handing the type to the driver itself (a raw query's arguments). Without ColumnType, the column follows the type's kind: a Phone string is TEXT, or a VARCHAR with @orm.Char{Max: n}. orm.JSON[T] is such a type, through Scan and Value.

Custom Attributes ​

An attribute is any value with an Apply(*orm.FieldOptions) method; written after a field, it is applied in its turn like the built-in ones, which are written the same way. A Validate(v any) error method adds a write check:

go
type Email struct{ Unique bool }

func (e Email) Apply(o *orm.FieldOptions) {
	o.MaxLength, o.Lower, o.Trim = 254, true, true
	o.Unique = o.Unique || e.Unique
}

func (Email) Validate(v any) error {
	if !strings.Contains(v.(string), "@") {
		return errors.New("is not an email address")
	}
	return nil
}

// vukaTargets says where Email may be written in .vuka files: on fields.
func (Email) vukaTargets() [vuka.OnField]struct{} { return [vuka.OnField]struct{}{} }

type User struct {
	orm.Model[User]
	ID    int64
	Email string
}

func (u *User) Schema(s *orm.Schema) { s.Field(&u.Email, Email{Unique: true}) }

Validate gets the value without its pointer or Option, and isn't called for NULL. orm.FieldOptions has every setting the built-ins use:

FieldOptions
Column, Skip, PK, Unique, Index, UniqueIndex, Null, Typethe column and its keys, DBType's type
MaxLength, MinLength, Pattern, Choices, Min, Max, Checksthe write checks (Min, Max are vuka.Option[int64]; Checks functions of the value)
Trim, Lowertext normalized as it is written
Default, Initialthe table's default (SQL), the value Create gives an empty field
AutoNow, AutoNowAdd, Date, Precision, UTCtimes
Size, Unsignedintegers
Manual, UUIDkeys
Comment, Collation, Label, Helpthe column's details, the field's description
OnDelete, Related, FK, Through, CrossDB, Dimsrelations, vectors

Set what the attribute means, and leave the rest: a member left at its zero value keeps what an earlier attribute set.

Deciding Fields at Run Time ​

What only the running program knows — the columns a legacy table it read at start lacks — a model attribute decides as the ORM maps the model: its ApplyModel sets ModelConfig.Fields, a function giving a field its FieldOptions over its attributes (func(owner reflect.Type, f reflect.StructField) (orm.FieldOptions, bool), false for a field it says nothing of), asked once per field before the model's first query. orm.SetFieldOptions (deprecated) does the same for every model.

Attribute Targets ​

The ORM's attribute types declare where they go (vuka.OnField; orm.Meta on the embedded orm.Base or the type, orm.TxOn and orm.Retry on functions), so @orm.Char above a function is a compile error, and an editor completing @orm. after a field offers the field attributes. In Go a type declares them with a vukaTargets method, as Email does above; it matters only to .vuka files using the attribute.

Reading Field Info ​

Fields() on a model's objects lists its columns as forms, admin pages and tools need them:

go
for _, f := range orm.Objects[Order]().Fields() {
	fmt.Println(f.Column, f.Label, f.Help, f.MaxLength, f.Choices, f.Null)
}

Each orm.FieldInfo has the field's Name, Column, Type, PK, Null, Unique, Index, MaxLength, MinLength, Min, Max, Choices, Pattern, Default, Initial, Label, Help, Comment, AutoNow and AutoNowAdd.

Fields in Migrations ​

orm makemigrations writes every column property the attributes set, and diffs them when they change:

go
var orderFields = []m.NamedField{
	m.F("ref", m.Varchar(12).Collation("utf8mb4_bin").Comment("the customer's reference")),
	m.F("status", m.Varchar(20).DefaultSQL("'draft'")),
	m.F("qty", m.SmallInt().Unsigned().DefaultSQL("1")),
	m.F("placed", m.Time().Precision(3).DefaultSQL("CURRENT_TIMESTAMP")),
	m.F("due", m.Date()),
	m.F("phone", m.Custom().Types(m.Dialects{Postgres: "VARCHAR(20)", MySQL: "VARCHAR(20)", SQLite: "VARCHAR(20)"})),
	m.F("id", m.UUID().PK()),
}
PropertyPostgresMySQLSQLite
CommentCOMMENT ON COLUMNCOMMENT '…'—
CollationCOLLATE "name"COLLATE nameCOLLATE name
PrecisionTIMESTAMP(n)DATETIME(n) (6 by default)—
SizeSMALLINT, INTEGER, BIGINTthe samethe same
Unsigned— (checked on writes)UNSIGNED— (checked on writes)
DefaultNowCURRENT_TIMESTAMP, CURRENT_DATECURRENT_TIMESTAMP(n), (CURRENT_DATE)CURRENT_TIMESTAMP, CURRENT_DATE
a DateFieldDATEDATEDATE
a UUID keyuuidCHAR(36)CHAR(36)

A changed default, comment, collation, precision, size, nullability, unique constraint or index is an m.AlterField; orm migrate applies it (SQLite by rebuilding the table, as for any change). A field with no default of its own keeps the one a migration gave it (an AddField's value for the rows that existed). Choices, Min, Max, Pattern, Label and Help are the program's alone and never reach the table. Unmanaged models are never migrated.

Released under the MIT License. Vuka builds on Go and its tools, which are made by the Go Authors. Vuka is not affiliated with Google or the Go project. Go is a trademark of Google LLC.