Models reference
How the db package maps a struct to a table. See Define models and save data for a walkthrough.
Tags
| Tag | Meaning |
|---|---|
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
| Field | Column |
|---|---|
Exported field with a db tag | The tag’s name |
| Exported field without a tag | snake_case of the field name: AuthorID → author_id, HTTPStatus → http_status, UserIDs → user_ids |
Field with a rel tag | A relation, not a column (see Relations) |
| Untagged struct, pointer-to-struct or slice-of-struct field | Not a column. Types that scan themselves (time.Time, sql.Null[T], anything implementing sql.Scanner) are columns |
Embedded struct without a db name | Its fields are flattened into the model (db.Model, db.Timestamps, your own mixins). Embedded pointers are allocated when scanning |
| Unexported field | Not 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
| Type | Columns | Effect |
|---|---|---|
db.Model | id (int64, primary key), created_at, updated_at | Auto-increment key plus db.Timestamps |
db.Timestamps | created_at, updated_at (time.Time) | Set by Create (if zero), Update, Save, mass Update, soft deletes and Upsert |
db.SoftDeletes | deleted_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:
| Method | Runs |
|---|---|
BeforeSave, AfterSave | Around Create, Update and Save (outside the create/update hooks) |
BeforeCreate, AfterCreate | Around Create, Save of a new row, and each row of CreateMany |
BeforeUpdate, AfterUpdate | Around Update and Save of an existing row |
BeforeDelete, AfterDelete | Around 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.
| Kind | Field type | Keys | Options (defaults) |
|---|---|---|---|
belongs_to | *R | This model’s fk → R’s references | fk (the field’s name in snake_case + _id), references (R’s primary key) |
has_one | *R | R’s fk → this model’s local | fk (this type’s name in snake_case + _id), local (the primary key) |
has_many | []R | R’s fk → this model’s local | as has_one |
many_to_many | []R | pivot.fk → this model’s primary key, pivot.related_fk → R’s primary key | pivot (both type names in snake_case, sorted, joined with _), fk (this type + _id), related_fk (R’s type + _id) |
| API | Does |
|---|---|
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
| Function | Does | Errors |
|---|---|---|
db.Create(ctx, &row) | INSERT; sets the generated key and timestamps | Hook errors, driver errors |
db.CreateMany(ctx, rows) | Multi-row INSERT in batches; sets keys on PostgreSQL and SQLite | Mixed rows (some with keys set, some without) |
db.Update(ctx, &row) | UPDATE of every column except the key, created_at and deleted_at, by key | Zero key; db.ErrNotFound if the row doesn’t exist |
db.Save(ctx, &row) | Create if the integer key is zero, else Update | Non-integer key; db.ErrNotFound as for Update |
db.Delete(ctx, &row) | Soft delete for SoftDeletes models, else DELETE | db.ErrNotFound if no (non-deleted) row matched |
db.ForceDelete(ctx, &row) | DELETE, even for SoftDeletes models | db.ErrNotFound |
db.Restore(ctx, &row) | Clears deleted_at | Model 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 key | db.ErrNotFound (a 404 in handlers) |