Skip to content

Models ​

Introduction ​

A model is the Go type that stands for a table. You write it as a struct embedding orm.Model[T]: its fields are its columns, its Schema method says what the columns are, and orm.Objects[Post]() is its manager.

Defining Models ​

Here are three models — users, tags and posts — with their Schema methods:

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

func (u *User) Schema(s *orm.Schema) {
	s.Field(&u.Name, orm.Char{Max: 100, Unique: true})
	s.Field(&u.Email, orm.Char{Max: 200})
}

type Tag struct {
	orm.Model[Tag]
	ID   int64
	Name string
}

func (t *Tag) Schema(s *orm.Schema) {
	s.Field(&t.Name, orm.Slug{Unique: true})
}

type Post struct {
	orm.Model[Post]
	ID        int64
	Title     string
	Slug      string
	Body      string
	Author    orm.FK[User]
	Tags      orm.M2M[Tag]
	Published vuka.Option[time.Time]
	Views     int
}

func (p *Post) Schema(s *orm.Schema) {
	s.Model(orm.Meta{Table: "posts", Ordering: []string{"-published"}})
	s.Field(&p.ID, orm.PK{})
	s.Field(&p.Title, orm.Char{Max: 200}, orm.Trigram{})
	s.Field(&p.Slug, orm.Slug{Unique: true})
	s.Field(&p.Body, orm.LongText{})
	s.Field(&p.Author, orm.Rel{OnDelete: orm.Cascade, Related: "posts"})
	s.Field(&p.Published, orm.Index{})
	s.Field(&p.Views, orm.DefaultTo(0))
}

Attributes are Go values, not struct tags: orm.Char{Max: 200} is a composite literal, type-checked, so a wrong setting is a compile error on the attribute.

Schema names each field by its address (&p.Title), so a renamed field is a compile error too. A field with nothing to say isn't listed. The ORM calls Schema once, on a zero value.

Row Methods ​

orm.Model[Post], embedded by value, gives the rows their methods:

p.Save(ctx)inserts a new row (its key filled in), updates a saved one
p.SaveFields(ctx, PostFields.Title, …)updates only those columns (or names: "title")
p.Refresh(ctx)reads the row again
p.Delete(ctx)deletes it
p.Load(ctx, PostFields.Tags, …)reads relations into the row
p.Add(ctx, "tags", rows…), Remove, Setchanges a many-to-many's links

Named Queries ​

orm.Objects[Post]() is the model's manager. A named query is a package-level function returning a QuerySet:

go
// PostLive is the published posts.
func PostLive() orm.QuerySet[Post] {
	return orm.Objects[Post]().Filter(PostFields.Published.IsNull(false))
}

You call it like any other QuerySet:

go
n, err := PostLive().Count(ctx)

Model Options ​

s.Model(orm.Meta{…}) in Schema describes the model as a whole:

orm.Meta field
Tablethe table; the plural of the type's name in snake_case otherwise (Post → posts)
Orderingthe order of a query that sets none, as OrderBy strings: []string{"-published", "id"}
Indexesits indexes, as an Indexes() method returns them
DBthe database it lives on; the default otherwise
Read, Writewhere its reads and its writes go, over DB
Mirrora database its writes are repeated on
Unmanagedthe database's own table: never created or migrated

A Meta() orm.Meta method works too, and wins.

Registering Models ​

orm.Register[Entry]() at init registers a model at start-up, so orm.Check and the migration commands know it without orm generate:

go
type Entry struct {
	orm.Model[Entry]
	ID   int64
	Note string
}

func (e *Entry) Schema(s *orm.Schema) { s.Model(orm.Meta{Table: "audit_entries"}) }

func init() { orm.Register[Entry]() }

Field Attributes ​

Attributes say what tags say, and win over them. In Go each is the value given to s.Field after the field's address: @orm.Char{Max: n} is orm.Char{Max: n}, and a bare @orm.Unique is orm.Unique{}. The table lists them in their Vuka spelling:

AttributeColumn
@orm.PK{Manual, UUID, …}the primary key (ID is one by default)
@orm.Char{Max: n, Unique, Index, Default, Choices, …}VARCHAR(n)
@orm.Int{Min, Max, Size, Unsigned, …}an integer column, bounded
@orm.DateField{…}, @orm.Datetime{Precision, UTC, DefaultNow, …}DATE, a timestamp
@orm.Slug{Max, Unique}an indexed VARCHAR, 50 by default
@orm.LongTextTEXT (a string's default)
@orm.Unique, @orm.Indexa unique constraint, an index
@orm.UniqueTogether("name")a named unique index: the fields sharing the name are unique together
@orm.Column("name")the column's name; the field's in snake_case otherwise
@orm.DBType("jsonb")the database type, written as is
@orm.Nullthe column takes NULL, for a type with no nil (an orm.FK that may be unset)
@orm.Skipno column
@orm.DefaultTo(v)the column default, a Go value written as SQL: @orm.DefaultTo(0), @orm.DefaultTo("draft")
@orm.DefaultSQL("CURRENT_TIMESTAMP")the column default, in SQL
@orm.AutoNow, @orm.AutoNowAddset to the time on every save, on create
@orm.Rel{OnDelete, Related, Column, Through, Null, CrossDB}a relation's: below
@orm.Dims(n), @orm.HNSW{Metric, M, EfConstruction}, @orm.IVFFlat{Metric, Lists}a vector and its pgvector index
@orm.Trigram, @orm.FullText{Config}text search indexes
@orm.SearchOf{Fields: []string{"title:A", "body:B"}, Config}an orm.TSVector generated from fields

Any number of attributes may follow a field. Fields has every setting of the column attributes — defaults, the checks a write makes, comments and collations — plus value types and attributes of your own.

Column Types ​

Column types follow the Go type: integers, floats, bool, string, time.Time, []byte; orm.JSON[T] (a JSON column), orm.CSV[E] (a comma-separated list), orm.Vector, orm.TSVector.

Relations ​

Relations are fields too. A post has an author and a set of tags; the field types below hold both the key and, once loaded, the related rows.

Foreign Keys ​

orm.FK[T] is a foreign key as one field: the column <field>_id, and the related row once it is loaded (SelectRelated, PrefetchRelated, Get). This fragment builds two posts and reads an author:

go
p := Post{Title: "Go generics", Author: orm.FKOf(&ada)} // from a row
q := Post{Title: "SQL", Author: orm.FKID[User](7)}       // from a key

author, err := p.Author.Get(ctx).Get() // read once, then held
if err != nil {
	return err
}
name := p.Author.Related().Name // nil until loaded
orm.FKOf(&row), orm.FKID[T](key)an FK to a row, to a key
f.Get(ctx)the related row: the one held, else read and held
f.Related(), f.Loaded()the row held (nil when not loaded); whether one is
f.ID(), f.Key(), f.IsSet()the key as int64, as it is; whether there is one
f.Set(&row), f.SetID(key)point it elsewhere

Its JSON is the key.

Many-to-Many Relations ​

orm.M2M[T] is a many-to-many, through a table between (<table>_<field>, posts_tags, or Rel's Through). This fragment adds, reads, removes and replaces a post's tags:

go
if err := p.Tags.Add(ctx, &golang, &tools); err != nil {
	return err
}
tags, err := p.Tags.All(ctx).Get() // the rows held, else read
if err != nil {
	return err
}
if err := p.Tags.Remove(ctx, &tools); err != nil {
	return err
}
if err := p.Tags.Set(ctx, &golang); err != nil { // its only links
	return err
}
return p.Tags.Clear(ctx)

Its methods are All, Add, Remove, Set, Clear (on a saved row; rows or keys), Rows and Loaded (what PrefetchRelated loaded).

A model's typed fields go on through relations into the related model: PostFields.Author.Name, PostFields.Tags.Name.

Relation Options ​

orm.Rel describes a relation:

orm.Rel field
OnDeletewhat deleting the related row does: orm.Cascade, orm.SetNull, orm.Restrict
Relatedthe inverse's name on the related model ("posts": User's posts, for orm.Q{"posts__views__gt": 60})
Columnthe foreign key's column
Througha many-to-many's table between
Nullthe key may be NULL
CrossDBthe related model is on another database: no constraint, prefetched rather than joined

NOTE

Reverse relations have no field type yet: filter through them with the engine's lookups, orm.Q{"posts__views__gt": 60}.

Nullable Fields ​

An Option[T] field is a nullable column: None is NULL. You set it and filter on its NULLs like this:

go
p.Published = vuka.Some(time.Now())
drafts, err := orm.Objects[Post]().Filter(PostFields.Published.IsNull(true)).All(ctx)

Its typed field is an orm.Field[V] with IsNull for its NULLs.

A Go model writes a nullable field as a pointer, Published *time.Time (nil is NULL), the usual Go, or as a vuka.Option[time.Time], what a Vuka model's Option[T] is and what vuka eject keeps: the ORM, JSON and an island's props read both. orm generate writes a row scanner for either.

Models in Plain Go ​

A Go model embeds orm.Model[T] (the type orm.Base stands for in a Vuka model) and says in its Schema method what a Vuka model says in attributes. s.Model(…) takes the model's (orm.Meta, any orm.ModelAttr), and s.Field(&p.X, …) a field's — the same values: @orm.Char{Max: 200} on Title is s.Field(&p.Title, orm.Char{Max: 200}).

The statics are plain names:

VukaGo
Post.Objectsorm.Objects[Post](); orm.For[Post](opts…) for a manager with options
Post.Title (a field reference)PostFields.Title, written by ormgen
func Post.Live() orm.QuerySet[Post]func PostLive() orm.QuerySet[Post]
@orm.Entityorm.Register[T]() at init; orm.Check(models…) takes others

Generated Fields ​

PostFields comes from ormgen — //go:generate go run github.com/vuka-lang/orm/v2/cmd/ormgen beside the models — as an orm.Field[V] (an orm.TextField[V] for strings) per column and a field per relation (PostFields.Author.Name, PostFields.Tags). It is a convenience: without it every query takes the engine's strings, orm.Q{"name__startswith": "a"} and OrderBy("-views"), and an unknown lookup is a runtime error.

Mixing Go and Vuka Models ​

The engine is the same, so Go and Vuka models live side by side, share databases and relate to each other. orm.Check() reports models the ORM can't map — a Schema given something other than the address of one of the receiver's fields among them; vukavet finds those in the source. orm: struct tags and orm.SetFieldOptions(fn) still work, deprecated for Schema.

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.