Skip to content
Models reference

Models reference

How the db package maps a struct to a table. See Define models and save data for a walkthrough.

Tags

TagMeaning
db:"title"Column name
db:"-"Not a column
db:"code,pk"Primary key (otherwise the id column, if there is one). One per model: composite keys work only through raw SQL
db:"meta,json"Stored as JSON text: encoded on write, decoded on read. A nil map, slice or pointer is stored as NULL
db:"total,readonly"Read but never written (generated columns, database defaults)
db:",pk"Options with the default column name

Columns

FieldColumn
Exported field with a db tagThe tag’s name
Exported field without a tagsnake_case of the field name: AuthorID → author_id, HTTPStatus → http_status, UserIDs → user_ids
Field with a rel tagA relation, not a column (see Relations)
Untagged struct, pointer-to-struct or slice-of-struct fieldNot a column. Types that scan themselves (time.Time, sql.Null[T], anything implementing sql.Scanner) are columns
Embedded struct without a db nameIts fields are flattened into the model (db.Model, db.Timestamps, your own mixins). Embedded pointers are allocated when scanning
Unexported fieldNot a column

Two fields mapping to the same column is an error. db.Columns[T]() lists a model’s columns in this order, and anetos gen declares a typed column for each.

Tables

The table is the snake_case plural of the type name, pluralizing the last word: Post → posts, BlogPost → blog_posts, Category → categories, Box → boxes, Person → people. Anything else:

// illustrative
func (Tag) TableName() string { return "tag_catalog" }

Embeddable types

TypeColumnsEffect
db.Modelid (int64, primary key), created_at, updated_atAuto-increment key plus db.Timestamps
db.Timestampscreated_at, updated_at (time.Time)Set by Create (if zero), Update, Save, mass Update, soft deletes and Upsert
db.SoftDeletesdeleted_at (*time.Time)Delete sets it; queries exclude those rows unless WithTrashed/OnlyTrashed; Restore clears it; Trashed() reports it

Timestamps are only managed when db.Timestamps (or db.Model) is embedded; a plain CreatedAt field is an ordinary column. Timestamps are written in UTC with microsecond precision (use TIMESTAMPTZ or TIMESTAMP(6) on PostgreSQL, DATETIME(6) on MySQL). Update never writes deleted_at, so saving an old copy of a row can’t undelete it.

Primary keys

An integer primary key that is zero on Create is generated by the database and set on the struct (RETURNING on PostgreSQL and SQLite, LastInsertId on MySQL). A non-zero integer key, or a key of another type (a UUID string, say), is inserted as given. Save needs an integer key.

Field types

Anything database/sql and the driver can read and write: strings, numbers, bools, []byte, time.Time, pointers to those (for nullable columns), sql.Null[T], and types implementing sql.Scanner and driver.Valuer. Times are always returned in UTC. NULL in a non-pointer field is an error that names the fix.

Time arguments are converted to UTC before they are sent. Use anetos.Date for DATE columns: it is written as 2026-03-15 text and read back as the same day, while a time.Time at a local midnight east of Greenwich becomes the previous day in UTC. NULL reads as the zero Date. See times and dates.

Hooks

Methods on the model’s pointer type, all func(ctx context.Context) error:

MethodRuns
BeforeSave, AfterSaveAround Create, Update and Save (outside the create/update hooks)
BeforeCreate, AfterCreateAround Create, Save of a new row, and each row of CreateMany
BeforeUpdate, AfterUpdateAround Update and Save of an existing row
BeforeDelete, AfterDeleteAround Delete and ForceDelete

A hook’s error stops the operation and is returned. Mass updates and deletes on a query, Restore and Upsert run no hooks.

Relations

Since v0.1.1. A relation field has a rel tag, rel:"kind" or rel:"kind,option=value,…", and no db tag. See Relations and eager loading.

KindField typeKeysOptions (defaults)
belongs_to*RThis model’s fk → R’s referencesfk (the field’s name in snake_case + _id), references (R’s primary key)
has_one*RR’s fk → this model’s localfk (this type’s name in snake_case + _id), local (the primary key)
has_many[]RR’s fk → this model’s localas has_one
many_to_many[]Rpivot.fk → this model’s primary key, pivot.related_fk → R’s primary keypivot (both type names in snake_case, sorted, joined with _), fk (this type + _id), related_fk (R’s type + _id)
APIDoes
db.RelOf[T, R](field)db.Rel[T, R], the handle of a relation field (anetos gen writes them in TRels). Nothing is checked until use; Err() reports a wrong field or type, a bad tag or missing key columns, as queries do before running
rel.With(nested...)Also load relations of the related rows
rel.Where(conds...), rel.OrderBy(orders...)Conditions and order of the relation’s query (default order: R’s primary key; a has_one gets the first row). Many-to-many queries join the pivot: qualify columns it also has
rel.WithTrashed()Include soft-deleted related rows
q.With(rels...)Load relations of the query’s rows: one query per relation (keys in chunks of 1,000) in Get, First, Find, Paginate and CursorPaginate; All returns an error
db.Load(ctx, &row, rels...), db.LoadMany(ctx, rows, rels...)The same for rows you have; loading again replaces the fields
q.WhereHas(rel, conds...), q.WhereDoesntHave(rel, conds...)EXISTS / NOT EXISTS subquery on the related rows (soft-deleted ones excluded unless rel.WithTrashed()); no nested relations
db.Attach(ctx, &row, rel, ids...)Insert the missing pivot rows (many_to_many only); needs a primary key or unique index on the two pivot columns
db.Detach(ctx, &row, rel, ids...)Delete those pivot rows; no ids: nothing
db.DetachAll(ctx, &row, rel)Delete all of the row’s pivot rows
db.Sync(ctx, &row, rel, ids...)Make the pivot rows exactly ids

A loaded pointer relation with no match is nil; a loaded slice relation with none is empty, not nil. Parents with the same key share one loaded *R (and the same slice contents, clipped so appending to one doesn’t change another). Passing one relation twice to With is an error.

Functions

FunctionDoesErrors
db.Create(ctx, &row)INSERT; sets the generated key and timestampsHook errors, driver errors
db.CreateMany(ctx, rows)Multi-row INSERT in batches; sets keys on PostgreSQL and SQLiteMixed rows (some with keys set, some without)
db.Update(ctx, &row)UPDATE of every column except the key, created_at and deleted_at, by keyZero key; db.ErrNotFound if the row doesn’t exist
db.Save(ctx, &row)Create if the integer key is zero, else UpdateNon-integer key; db.ErrNotFound as for Update
db.Delete(ctx, &row)Soft delete for SoftDeletes models, else DELETEdb.ErrNotFound if no (non-deleted) row matched
db.ForceDelete(ctx, &row)DELETE, even for SoftDeletes modelsdb.ErrNotFound
db.Restore(ctx, &row)Clears deleted_atModel without SoftDeletes
db.Upsert(ctx, rows, conflict, update...)INSERT … ON CONFLICT / ON DUPLICATE KEY UPDATE; no update columns means “keep the existing row”Unknown column names
db.Find[T](ctx, id)SELECT by keydb.ErrNotFound (a 404 in handlers)