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 |
Traversal is UUID-keyed only
Foreign keys are always stored as uuid, so relation traversal is generated only between
UUID-keyed models. A model with an integer primary key can still be defined, but FK
targets pointing at it are skipped.
Required foreign key — *Model#
*Model is a required, non-null reference. It generates a scalar FK column
(author: *User → an author_id: uuid column) 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 Option<uuid>.
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.
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
thumbnails: [char(255); 5] // 5 fixed-size byte arrays
}Because the element must be fixed-size, [string; N] is invalid — use char(N) for
fixed-width text inside an array.
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 Address {
street: char(100)
city: char(50)
zip: char(10)
}
User {
id: +uuid
address: Address? // optional embedded struct
}Structs are fixed-size only
A struct may contain only fixed-size fields. string, relations, and nested
variable-length types are rejected inside a struct — use char(N) for text. Reference a
struct required (address: Address) or optional (address: Address?).
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, ... }.