import "github.com/devituz/lagodev/orm"
type User struct {
orm.Model // ID, CreatedAt, UpdatedAt, DeletedAt
Name string
Email string `column:"email" orm:"unique"`
IsAdmin bool `column:"is_admin"`
Meta any `column:"meta" orm:"cast:json"`
}orm.Model embeds the canonical timestamp + primary-key fields. Use the
lighter orm.Timestamps if you don't want soft deletes.
| Tag | Purpose |
|---|---|
column:"name" |
Map a field to a specific column (default: snake_case) |
column:"-" |
Skip the field entirely (don't read/write) |
orm:"primary" |
Mark a field as primary key (default: ID) |
orm:"autoincrement" |
Mark an integer primary key auto-incrementing |
orm:"nullable" |
Allow NULL |
orm:"hidden" |
Hide from ToMap() serialization |
orm:"fillable" |
Allow mass-assignment |
orm:"cast:NAME" |
Apply a registered cast (json, bool, int, date, …) |
orm:"default:VALUE" |
Document a default (for schema generation) |
func (User) TableName() string { return "app_users" }orm.Query[T](conn) returns a typed *Builder[T].
import "github.com/devituz/lagodev/orm"
// Simple list
var users []User
err := orm.Query[User](conn).Get(ctx, &users)
// Filters
err := orm.Query[User](conn).
Where("is_admin", "=", true).
Where("created_at", ">", "2026-01-01").
OrderBy("id", "desc").
Limit(20).
Get(ctx, &users)
// First (returns ErrNotFound when missing)
ada, err := orm.Query[User](conn).
Where("email", "=", "ada@example.com").
First(ctx)
// Find by PK
u, err := orm.Query[User](conn).Find(ctx, 42)
// Pluck a single column
emails, err := orm.Pluck[User, string](ctx,
orm.Query[User](conn).Where("is_admin", "=", true),
"email")n, err := orm.Query[User](conn).Count(ctx)
exists, _ := orm.Query[User](conn).Where("email", "=", e).Exists(ctx)
total, _ := orm.Query[User](conn).QB().Sum(ctx, "balance")
avg, _ := orm.Query[User](conn).QB().Avg(ctx, "score")If your model embeds orm.Model and your table has a deleted_at
column, the default scope hides deleted rows:
var live []User
_ = orm.Query[User](conn).Get(ctx, &live) // deleted_at IS NULL
// Include trashed
_ = orm.Query[User](conn).WithTrashed().Get(ctx, &allIncludingDeleted)
// Only trashed
_ = orm.Query[User](conn).OnlyTrashed().Get(ctx, &trashed)Where("col", op, value) accepts =, <>, <, <=, >, >=,
like, ilike. Use the dedicated methods for everything else:
.WhereIn("id", []int{1, 2, 3})
.WhereNotIn("status", []string{"banned", "deleted"})
.WhereNull("verified_at")
.WhereNotNull("verified_at")
.WhereBetween("age", 18, 65)
.WhereRaw("LOWER(name) = LOWER(?)", "Ada")orm.Query[User](conn).
Where("active", "=", true).
Where(func(q *query.Builder) {
q.Where("role", "=", "admin").OrWhere("role", "=", "owner")
}).
Get(ctx, &out)Produces WHERE active = ? AND (role = ? OR role = ?).
orm.Save(ctx, conn, &model) is the single entry point:
- inserts when
model.ID == 0 - updates otherwise
- populates
CreatedAt/UpdatedAtautomatically (inconn.Location()) - fires
BeforeSave/BeforeCreate/AfterCreate/AfterSavehooks
u := &User{Name: "Ada", Email: "ada@example.com"}
_ = orm.Save(ctx, conn, u) // INSERT, u.ID now non-zero
u.Name = "Augusta"
_ = orm.Save(ctx, conn, u) // UPDATEorm.Delete(ctx, conn, &u) // soft-delete if available, else hard
orm.ForceDelete(ctx, conn, &u) // always hard-delete
orm.Restore(ctx, conn, &u) // unset deleted_atImplement any subset of the hook interfaces on your model:
type User struct { orm.Model; Email string }
func (u *User) BeforeCreate(*orm.HookContext) error {
u.Email = strings.ToLower(u.Email)
return nil
}
func (u *User) AfterCreate(ctx *orm.HookContext) error {
return notifyWelcomeEmail(ctx.Ctx, u)
}Available: BeforeCreate, AfterCreate, BeforeUpdate, AfterUpdate,
BeforeSave, AfterSave, BeforeDelete, AfterDelete, AfterFind.
orm:"cast:NAME" runs a casts.Cast on the value when reading from the
database and writing back. Built-in casts: json, jsonb, bool,
boolean, int, integer, date.
Register custom casts:
import "github.com/devituz/lagodev/casts"
casts.Register("encrypted", encryptedCast{key: getKey()})err := conn.Transaction(ctx, func(tx *database.Tx) error {
if err := orm.Query[Account](conn).WithTx(tx).
Where("id", "=", from).Update(ctx, map[string]any{"balance": -100}); err != nil {
return err
}
return orm.Query[Account](conn).WithTx(tx).
Where("id", "=", to).Update(ctx, map[string]any{"balance": +100})
})The closure receives a *Tx that satisfies database.Executor — pass it
to WithTx() on a query builder, or call its methods directly. Roll-back
happens automatically if the closure returns non-nil.
conn.Transaction(ctx, func(tx *database.Tx) error {
_ = tx.Savepoint(ctx, "sp1")
if err := dodgyOperation(tx); err != nil {
_ = tx.RollbackTo(ctx, "sp1")
}
return nil
})When the builder isn't enough, drop to *sql.DB:
rows, err := conn.QueryContext(ctx, "SELECT id, complex(:filter) FROM x", args...)conn.ExecContext / conn.QueryRowContext round out the API. SQL
logging picks them up automatically when DB_LOG_QUERIES=true.