Skip to main content
Version: 1.17.0

Modeling API Reference

The contract between a Go struct and a table: every struct tag Quark reads, the lifecycle hook interfaces it looks for, and the metadata it caches per model.

For worked examples — composite keys, soft delete, rich types, timezones — see the Modeling guide.

SymbolSignaturePurpose
BeforeCreateBeforeCreate(ctx context.Context) errorHook run before an insert.
AfterCreateAfterCreate(ctx context.Context) errorHook run after a successful insert.
BeforeUpdateBeforeUpdate(ctx context.Context) errorHook run before an update.
AfterUpdateAfterUpdate(ctx context.Context) errorHook run after a successful update.
BeforeDeleteBeforeDelete(ctx context.Context) errorHook run before a delete.
AfterDeleteAfterDelete(ctx context.Context) errorHook run after a successful delete.
TableNameTableName() stringOverride the default (snake-cased, pluralised) table name. Interface: quark.TableNamer.
ValidateValidate(ctx context.Context) errorModel-level validation, run before the validate tags on Create, CreateBatch, Upsert and UpsertBatch. Interface: quark.Validator.
GetModelMetaGetModelMeta[T any]() *ModelMetaReturn cached reflection metadata for model T.

Hooks are optional: implement the methods you need on the model's pointer receiver and Quark calls them at the matching point in the write path. Each method Quark looks for has an exported interface — quark.BeforeCreateHook, quark.TableNamer, quark.Validator and the rest — so a model can assert it at compile time, var _ quark.Validator = (*User)(nil), instead of finding out that a misspelt method was never called. The extension contract lists them all.

Struct Tags​

db - Column Mapping​

type User struct {
ID int64 `db:"id"` // Column name
FirstName string `db:"first_name"` // Explicit column name
Internal string `db:"-"` // Ignored (not persisted)
}

pk - Primary Key​

// Single primary key
type User struct {
ID int64 `db:"id" pk:"true"`
}

// Composite primary key
type Membership struct {
UserID int64 `db:"user_id" pk:"true"`
GroupID int64 `db:"group_id" pk:"true"`
}

rel - Relations​

type User struct {
ID int64 `db:"id" pk:"true"`
Posts []Post `rel:"has_many" join:"user_id"` // One-to-many
Profile Profile `rel:"has_one" join:"user_id"` // One-to-one
}

type Post struct {
ID int64 `db:"id" pk:"true"`
UserID int64 `db:"user_id"`
User *User `rel:"belongs_to" join:"user_id"` // Many-to-one
}

type Product struct {
ID int64 `db:"id" pk:"true"`
Tags []Tag `rel:"many_to_many" m2m:"product_tags:product_id:tag_id"` // Many-to-many
}

Relation Types:

TypeDirectionFK LocationTag
has_oneOne-to-oneRelated tablerel:"has_one" join:"fk_col"
has_manyOne-to-manyRelated tablerel:"has_many" join:"fk_col"
belongs_toMany-to-oneThis tablerel:"belongs_to" join:"fk_col"
many_to_manyMany-to-manyJoin tablerel:"many_to_many" m2m:"join_table:this_fk:other_fk"

quark - Constraints​

type User struct {
ID int64 `db:"id" pk:"true"`
Email string `db:"email" quark:"unique,not_null"`
Name string `db:"name" quark:"not_null"`

// Column rename for migrations
NewField string `db:"new_field" quark:"rename:old_field"`
}
OptionDescription
not_nullNOT NULL constraint
uniqueUNIQUE constraint
rename:oldRename column during sync

default - Default Values​

type User struct {
Status string `db:"status" default:"'active'"`
Count int `db:"count" default:"0"`
}

nullable - Null Handling​

type User struct {
// Explicitly allow NULL
DeletedAt *time.Time `db:"deleted_at" nullable:"true"`

// Explicitly disallow NULL
Email string `db:"email" nullable:"false"`
}

Soft Delete​

type User struct {
ID int64 `db:"id" pk:"true"`
Name string `db:"name"`
DeletedAt *time.Time `db:"deleted_at"` // Enables soft delete
}

// Query excludes soft-deleted by default
users, _ := quark.For[User](ctx, client).List()

// Include soft-deleted with Unscoped
allUsers, _ := quark.For[User](ctx, client).Unscoped().List()

Hooks​

BeforeCreateHook​

func (u *User) BeforeCreate(ctx context.Context) error {
u.CreatedAt = time.Now()
if u.Status == "" {
u.Status = "pending"
}
return nil
}

AfterCreateHook​

func (u *User) AfterCreate(ctx context.Context) error {
// Send welcome email, index in search, etc.
return eventBus.Publish(ctx, "user.created", u.ID)
}

BeforeUpdateHook​

func (u *User) BeforeUpdate(ctx context.Context) error {
u.UpdatedAt = time.Now()
return nil
}

AfterUpdateHook​

func (u *User) AfterUpdate(ctx context.Context) error {
// Invalidate cache, sync to external systems
return cache.Invalidate(ctx, fmt.Sprintf("user:%d", u.ID))
}

BeforeDeleteHook​

func (u *User) BeforeDelete(ctx context.Context) error {
// Clean up related resources
return cleanupUserFiles(u.ID)
}

AfterDeleteHook​

func (u *User) AfterDelete(ctx context.Context) error {
// Audit logging
return audit.Log(ctx, "user.deleted", u.ID)
}

Custom Table Name​

type User struct {
ID int64 `db:"id" pk:"true"`
Name string `db:"name"`
}

func (User) TableName() string {
return "app_users" // Custom table name
}

Metadata Access​

type User struct {
ID int64 `db:"id" pk:"true"`
Name string `db:"name"`
Email string `db:"email"`
Posts []Post `rel:"has_many" join:"user_id"`
}

meta := quark.GetModelMeta[User]()

fmt.Println(meta.Table) // => users
fmt.Println(meta.PK.Column) // => id
fmt.Println(meta.HasCompositePK) // => false

for _, field := range meta.Fields {
fmt.Printf("%s -> %s\n", field.Column, field.Type)
}
// => id -> int64
// => name -> string
// => email -> string

for name, rel := range meta.Relations {
fmt.Printf("%s: %s via %s\n", name, rel.Type, rel.JoinCol)
}
// => Posts: has_many via user_id (relations live in meta.Relations, not meta.Fields)

meta.Fields holds only the persisted columns; relations are kept separately in meta.Relations, keyed by Go field name. field.Type is a reflect.Type, so it prints as int64, string, and so on.