Relations
Modeling relationships in .forge — one-to-many, required/optional foreign keys, many-to-many, fixed arrays, and inline structs.
Relations connect models. From the relation syntax below, ForgeDB generates the persisted foreign-key columns, the junction tables, and typed traversal methods (forward, reverse, and eager-load).
Relation syntax at a glance#
| Syntax | Kind | Meaning |
|---|---|---|
[Model] | one-to-many | This parent has many children |
*Model | required FK | Must reference exactly one record |
?Model | optional FK | May reference one record, or none |
[..] / [..] | many-to-many | Two models each list the other |
A foreign key follows its target's identity
There is no separate FK type. An FK column is physically identical to the column the
target's identity field itself occupies — author: *User is a 16-byte uuid column when
User { id: +uuid }, an 8-byte u64 column when User { id: +u64 }, and a 3-byte inline
string when Airport { id: string(3!) }. Every relation
capability (forward getters, reverse getters, eager load, referential integrity,
@on_delete, many-to-many) is generated for whatever key the target has.
Required foreign key — *Model#
*Model is a required, non-null reference. It generates a scalar FK column of the
target's key type (author: *User → an author_id column that is a uuid, a u64, … —
whatever User's identity is) and a forward getter.
Post {
id: +uuid
title: string
author: *User // every post must have an author
}Existence is enforced: creating a Post with an author id that does not resolve is
rejected (HTTP 409 dangling reference).
Optional foreign key — ?Model#
?Model is a nullable reference — the FK column is the nullable form of the target's key
(Option<Uuid>, Option<u64>, …).
Post {
id: +uuid
category: ?Category // a post may have no category
}One-to-many — [Model]#
[Model] declares the many side from the parent. It is virtual — nothing is stored
on the parent row; the relationship lives on the child's foreign key. It generates a
reverse getter (e.g. user_posts) that is index-served.
User {
id: +uuid
posts: [Post] // reverse of Post.author
}
Post {
id: +uuid
author: *User
}Many-to-many#
Written [..] in the table above: both models list the other with a [OtherModel] field,
and neither side is a foreign key. The parser detects this as a many-to-many relationship
and generates a junction table.
Post {
id: +uuid
tags: [Tag]
}
Tag {
id: +uuid
posts: [Post]
}// Generated junction + traversal (illustrative):
struct PostTagLink { left: Uuid, right: Uuid }
db.link_post_tag(post_id, tag_id);
db.post_tags(post_id); // -> Vec<Tag>
db.tag_posts(tag_id); // -> Vec<Post>link / unlink maintain the junction; post_tags / tag_posts traverse it.
Each junction column is that endpoint's own key width, so a mixed pair works — a
+u64-keyed model may be linked to a +uuid-keyed one. A junction stores each endpoint's
id in a fixed-width, hashable column, which is the same thing being an identity already
requires. So any legal identity is a
legal endpoint, and there is no second rule to check here. An endpoint keyed on a *Model
foreign key resolves through to the terminal key type.
Fixed-size arrays — [type; N]#
A fixed-length array of a fixed-size element type (a scalar or a struct). The count is a numeric literal.
Product {
scores: [u32; 10] // exactly 10 ints
layer_digests: [bytes(32); 5] // 5 SHA-256 digests
}Because the element must be fixed-size, [string; N] is invalid — and bytes(N) is not a
substitute, since it stores raw bytes rather than text. There is no way to put text in a
fixed array; model a list of strings as a related model.
Inline structs#
A struct is a reusable embedded value type. It is inlined into the model that references it — not stored as its own table.
struct Dimensions {
length_mm: u32
width_mm: u32
height_mm: u32
}
Product {
id: +uuid
name: string // text lives on the model, not in the struct
packed: Dimensions? // optional embedded struct
}Structs are fixed-size only — which rules out text
A struct may contain only fixed-size fields. string, relations, and nested
variable-length types are rejected inside a struct, and bytes(N) is not a workaround —
it stores raw bytes, not text. So there is no way to embed an address, a name, or any free
text in a struct; put it on the model, or on a related model. Reference a struct required
(packed: Dimensions) or optional (packed: Dimensions?).
Generated traversal#
From these declarations ForgeDB generates typed helpers on Database:
- Forward FK —
post_author(&post) -> Option<User>(optional FKs thread throughand_then). - Reverse one-to-many —
user_posts(id) -> Vec<Post>, an O(matches) index probe. - Many-to-many —
link_post_tag,post_tags,tag_posts. - Eager load —
post_with_relations(id) -> PostWithRelations { post, author, ... }.