Skip to content

Scopes

A scope is a reusable, named read filter — the unit a team packages common conditions in (ActiveOnly, OwnedBy(user), Published) and shares across queries. The orm Repo applies scopes with Repo.Scopes(...), and because each is just a function over the query builder, scopes compose by chaining. This is the same idea as gorm’s Scopes, made type-safe.

For exhaustive API detail, see the reference at pkg.go.dev/liteorm.org/orm.

orm.Scope[T] is a function that receives the read’s builder and returns it, having added conditions:

type Scope[T any] func(*query.SelectBuilder[T]) *query.SelectBuilder[T]

Because it takes and returns the builder, a scope can add a Where, an OrderBy, a Limit, or anything else the builder exposes — and several scopes chain into one read. A parameterless scope is just such a function; a parameterized one is a constructor that returns a closure:

// A bare scope: no arguments, references only the row.
func Published(b *query.SelectBuilder[Post]) *query.SelectBuilder[Post] {
return b.Where("published_at IS NOT NULL")
}
// A parameterized scope: a constructor that closes over its argument.
func OwnedBy(id int64) orm.Scope[Post] {
return func(b *query.SelectBuilder[Post]) *query.SelectBuilder[Post] {
return b.Where("author_id = ?", id)
}
}

Pass one or more scopes to Repo.Scopes; they apply in order, and the result is a Repo view you finish with any read verb. The soft-delete scope still applies underneath, so a scoped read never leaks deleted rows by accident:

posts := orm.NewRepo[Post](db)
mine, _ := posts.
Scopes(Published, OwnedBy(ada.ID)).
OrderBy("created_at DESC").
Limit(10).
Find(ctx)

Scopes returns a new Repo view and leaves the original untouched — the scope slice is copied, so sibling views never alias each other’s chain. That means you can build a base view once and branch off it:

active := posts.Scopes(Published)
recent, _ := active.OrderBy("created_at DESC").Limit(10).Find(ctx)
count, _ := active.Scopes(OwnedBy(ada.ID)).Count(ctx)

Scopes composes freely with the Repo’s inline read surface (Where, Filter, OrderBy, Limit, Offset) and the finishers (Find, First, Count, Exists) — reach for a named scope when the filter is reused, and inline Where/Filter for a one-off. See CRUD with the Repo for that surface.

Soft-delete visibility is its own tri-state scope, orthogonal to the ones you write: IncludeDeleted() and OnlyDeleted() return Repo views that widen or invert the default “live rows only” filter. They chain with your scopes, so you can combine a business filter with a delete-visibility scope in the same read:

// published posts, including soft-deleted ones
archived, _ := posts.IncludeDeleted().Scopes(Published).Find(ctx)

The tri-state scopes are covered in full — with ForceDelete and Restore — in soft delete.