Skip to main content
Version: 1.8.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.
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.

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.