Skip to main content

Keys

A field named ID is the primary key. Integer keys are generated by the database, and the new value is copied back onto your struct.

By convention​

type Ticket struct {
ID int64
Title string
}

ticket := &Ticket{Title: "One"}

err := db.Tickets.Insert(ctx, ticket)

fmt.Println(ticket.ID) // set by the database

found, err := db.Tickets.Find(ctx, ticket.ID)

A zero integer key means "assign me one". This is the only place ant reads meaning into a zero value without being asked. A non-zero key is written as you gave it, even on a generated column, so imported rows keep their IDs.

imported := &Ticket{ID: 500, Title: "Imported"}

err := db.Tickets.Insert(ctx, imported)

A different key​

b.Key replaces the convention. An ID field on the same model becomes an ordinary column.

type Country struct {
ID int64
Code string
}

func (Country) Configure(b *ant.ModelBuilder[Country]) {
b.Key(&C.Code)
}

var C = ant.Fields[Country]()

latvia, err := db.Countries.Find(ctx, "LV")

Keys the application assigns​

NotGenerated says you always supply the key, so zero is a real value.

type Seat struct {
Number int
Row string
}

func (Seat) Configure(b *ant.ModelBuilder[Seat]) {
b.Key(&S.Number).NotGenerated()
}

var S = ant.Fields[Seat]()

String and UUID keys​

Keys that aren't integers are never generated, and an empty one is written as it is. A named type works as a key, and a plain string still finds it.

type Slug string

type Page struct {
ID Slug
Title string
}

err := db.Pages.Insert(ctx, &Page{ID: "home", Title: "Home"})

home, err := db.Pages.Find(ctx, "home")

Keys made in Go​

ant.GenerateKey fills in a zero key just before the insert. A key you supply is kept, and if the insert fails, the generated one is cleared.

type Order struct {
ID string
Total int
}

func (Order) Configure(b *ant.ModelBuilder[Order]) {
ant.GenerateKey(b, &O.ID, ids.UUID)
}

var O = ant.Fields[Order]()

The ids package has ids.UUID and ids.ULID. Both sort by creation time, so new rows land at the end of an index instead of scattering across it. For snowflakes:

flakes, err := ids.NewSnowflake(1) // the node number

next := flakes.Next()

Composite keys​

CompositeKey spans several columns. FindKey takes its parts in order. Tracking, Reload, and Remove all use the whole key.

type Grant struct {
UserID int64
Scope string
Level int
}

func (Grant) Configure(b *ant.ModelBuilder[Grant]) {
b.CompositeKey(ant.F(&G.UserID), ant.F(&G.Scope))
}

var G = ant.Fields[Grant]()

grant, err := db.Grants.FindKey(ctx, int64(1), "write")

Find needs a single key, so it returns ant.ErrNoKey here. The wrong number of parts is ant.ErrKeyType, and changing part of a tracked key is ant.ErrKeyChanged.

Sequences​

A column can draw from a named sequence, which migrations create for you.

func (Invoice) Configure(b *ant.ModelBuilder[Invoice]) {
b.Column(&I.Number).Sequence("invoice_numbers", ant.StartAt(1000), ant.IncrementBy(10))
}

No key​

A model with no ID field simply has no key. It inserts and queries like any other, and Find, DeleteByID, and Reload return ant.ErrNoKey. b.NoKey() does the same for a model whose ID column isn't unique.

type LogLine struct {
ID int64
Text string
}

func (LogLine) Configure(b *ant.ModelBuilder[LogLine]) {
b.NoKey()
}
The wrong key type

Find can't tie its key type to the model at compile time, so db.Users.Find(ctx, "1") compiles, and returns ant.ErrKeyType when it runs. An untyped number fits any integer key.