Indexes & projections
Single-field ^ indexes, composite @index, @projection partial reads, and the &unique constraint in .forge.
ForgeDB builds in-memory secondary indexes from your schema so lookups and filters skip a full-table scan. This page covers the four index and projection surfaces.
Single-field index — ^#
The ^ modifier indexes a field. Each indexed scalar gets a
find_by_<field> / get_by_<field> probe that resolves candidates in O(1) instead of
scanning, plus index-served REST filters.
Post {
id: +uuid
slug: ^string // find_by_slug(...)
views: ^u64
}Hash indexes are exact-match only
A default index is a hash index — it answers field = value exactly. It does not
answer prefix or range queries (views > 100, slug LIKE ...).
Ordered indexes by type#
Ordered and range queries are served automatically for non-nullable indexed fields of an
Ord-key type — u32, u64, i32, i64, timestamp, and decimal. These get a
parallel ordered index backing find_by_<field>_range(min, max, descending, limit), which
walks values in order (range and top-N in O(offset + limit)).
Post {
views: ^u64 // range/top-N served: find_by_views_range(...)
created_at: ^timestamp
}f64 (no clean total order), nullable fields, and string get the exact-match hash index
only. The choice follows field type — there is no directive to force it.
Unique — &unique#
The & modifier is the uniqueness constraint. On insert/update the generated code
probes the field's index and rejects a value already held by a different row (HTTP 409).
A unique field is also indexed, so lookups on it are fast.
User {
email: &string @email // unique, and find_by_email(...)
}Composite index — @index(a, b)#
A model-level @index(field1, field2, ...) builds one index keyed on the tuple of fields.
It answers exact-match on the full tuple (a = ? AND b = ?) via find_by_<a>_and_<b>.
Order {
id: +uuid
user_id: uuid
status: string
created_at: timestamp
@index(user_id, created_at)
@index(status, created_at)
}Rules:
- Must list at least two fields (a single-field index uses
^). - Every listed field must exist in the model.
- The key is a hash of the concatenated per-field keys — exact-match on the whole tuple, not prefix or range.
Projections — @projection(name: cols)#
A model-level @projection(name: col, col, ...) generates a tailored partial-read struct
<Model><Name> plus narrow reads (get_<name>, all_<name>, and the snapshot variants)
that materialize only the primary key and the named columns — never the full record.
Post {
id: +uuid
title: string
slug: ^string
views: ^u64
content: string
published_at: timestamp?
@projection(card: title, slug, published_at)
@projection(agg: views, published_at)
}@projection(card: title, slug)// Generated (illustrative):
struct PostCard { id: Uuid, title: String, slug: String }
db.post.get_card(id); // -> Option<PostCard>
db.post.all_card(); // narrow scan, skips other columnsProjections are exposed over REST as a closed set — GET /api/post?projection=card
selects a declared projection by name. There is no ad-hoc ?fields= column list; only
declared projection names are valid (an unknown name is a 400).
Projections shrink reads, not just the wire
A projected point-get (and the wasm replica path) skips reading the unselected columns
entirely. A projected REST list still reads full rows server-side to filter/sort — it
shrinks only the response body.