Scopes¶
Scopes bundle query constraints you use over and over. Local scopes become chainable methods on QueryBuilder. Global scopes apply automatically until you explicitly opt out — perfect for multi-tenant filters, published-only views, or soft deletes.
Arvel discovers scopes at class creation time and registers them on a per-model ScopeRegistry, so everything stays explicit and introspectable.
Local scopes¶
Decorate a static or class method with @scope. The method receives the current QueryBuilder and returns it (possibly chained further).
from arvel.data import ArvelModel
from arvel.data.scopes import scope
from arvel.data.query import QueryBuilder
class User(ArvelModel):
__tablename__ = "users"
@scope
@staticmethod
def active(query: QueryBuilder["User"]) -> QueryBuilder["User"]:
return query.where(User.is_active == True)
@scope
@staticmethod
def older_than(query: QueryBuilder["User"], age: int) -> QueryBuilder["User"]:
return query.where(User.age > age)
Call them like fluent methods:
await User.query(session).active().older_than(30).all()
Method names may be prefixed with scope_ — Arvel strips the prefix when registering so scope_active becomes .active().
Global scopes¶
Subclass GlobalScope, implement apply, and assign a name. Attach instances through __global_scopes__ on the model:
from arvel.data.scopes import GlobalScope
class PublishedScope(GlobalScope):
name = "PublishedScope"
def apply(self, query):
return query.where(Post.published_at.isnot(None))
class Post(ArvelModel):
__tablename__ = "posts"
__global_scopes__ = [PublishedScope()]
Every Post.query() starts life with those constraints unless you remove them.
Opting out¶
When you need the full table — admin tools, reporting, or tests — exclude scopes explicitly:
query.without_global_scope("PublishedScope")
query.without_global_scopes() # drop every global scope for this builder
Soft deletes integrate here: with_trashed() and only_trashed() manipulate the SoftDeleteScope without you remembering column names.
Composition¶
Local scopes are plain functions — call other helpers, accept parameters, and return the builder. Global scopes should stay pure filters: no I/O, no session side effects — just SQL shape.
Scopes are how a large codebase keeps queries readable. Instead of copying the same where clauses into dozens of controllers, you name an idea once and chain it everywhere.