Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

References and Derived Fields

References let one table point to another table’s primary key. Derived fields copy or assemble data from matching rows in another table.

FeatureWhat source data storesWhat runtime model gets
ref<Item.id>The target row id, such as 1001.The id value or a target-specific wrapper.
from = { ... }Rows stay in a child table.The parent row receives a copied/nested value.

Use ref when the relationship itself should remain an id. Use from when exported data should contain a convenient nested field.

The target of a ref must be a mode = "map" table, and the referenced field must be that table’s key.

References

[[tables.fields]]
name = "required_item"
type = "ref<Item.id>"

Sora validates that every value points to an existing row in the referenced table.

References are still stored as values in source data. The generated runtime can expose them as key values or target-specific wrapper types depending on the language backend.

References can be nested in containers such as list<ref<Item.id>>, set<ref<Item.id>>, or optional<ref<Item.id>>. The same primary-key rule applies to the inner ref.

Derived Fields

A derived field is not read from the current table’s cell. It is built from matching rows in another table.

This keeps editable data normalized while generated runtime models can expose convenient nested values. For example, quest rewards can be stored as two tables:

Quest:

idname
1001First Quest
1002Second Quest

QuestReward:

quest_idsort_orderitem_idcount
10011200110
1001220021
1002120035

At runtime, Quest may want a direct rewards: list<Reward> field. Declare that the field comes from QuestReward:

[[structs]]
name = "Reward"

[[structs.fields]]
name = "item_id"
type = "ref<Item.id>"

[[structs.fields]]
name = "count"
type = "i32"

[[tables]]
name = "Quest"
mode = "map"
key = "id"

[[tables.fields]]
name = "id"
type = "i32"

[[tables.fields]]
name = "name"
type = "string"

[[tables.fields]]
name = "rewards"
type = "list<struct<Reward>>"
from = { table = "QuestReward", parent_key = "id", child_key = "quest_id", order_by = "sort_order" }

[[tables]]
name = "QuestReward"
mode = "list"

[[tables.fields]]
name = "quest_id"
type = "ref<Quest.id>"

[[tables.fields]]
name = "sort_order"
type = "i32"

[[tables.fields]]
name = "item_id"
type = "ref<Item.id>"

[[tables.fields]]
name = "count"
type = "i32"

This means:

  • from.table = "QuestReward": read matching rows from the QuestReward child table.
  • from.parent_key = "id": use the parent row’s Quest.id value for matching.
  • from.child_key = "quest_id": match child rows where QuestReward.quest_id equals the parent key.
  • from.order_by = "sort_order": when several child rows match, sort them by the child table’s sort_order field in ascending order.

With the example data above, Quest.id = 1001 receives two reward rows, ordered as 2001, then 2002.

The exported parent row is shaped as if rewards had been written directly on Quest:

{
  "id": 1001,
  "name": "First Quest",
  "rewards": [
    {"item_id": 2001, "count": 10},
    {"item_id": 2002, "count": 1}
  ]
}

The field type controls how many child rows may match:

Field typeMatch countResult when no row matches
list<T>zero or moreempty list
optional<T>zero or onenull
Texactly onevalidation error

If T or optional<T> matches more than one child row, Sora reports an error.

Copying One Child Field

Without from.field, Sora assembles a struct from child table fields with the same names as the struct fields.

When the parent should receive one field from the child row instead, set from.field:

[[unions]]
name = "EventCondition"
tag = "type"

[[unions.variants]]
name = "QuestCompleted"

[[unions.variants.fields]]
name = "quest_id"
type = "ref<Quest.id>"

[[unions.variants]]
name = "HasItem"

[[unions.variants.fields]]
name = "item_id"
type = "ref<Item.id>"

[[unions.variants.fields]]
name = "count"
type = "i32"

[[tables.fields]]
name = "condition"
type = "union<EventCondition>"
from = { table = "EventConditionEntry", parent_key = "id", child_key = "event_id", field = "value" }

[[tables]]
name = "EventConditionEntry"
mode = "list"

[[tables.fields]]
name = "event_id"
type = "ref<Event.id>"

[[tables.fields]]
name = "value"
type = "union<EventCondition>"
parser = { kind = "tagged_columns", prefix = "" }

This means Event.condition receives EventConditionEntry.value for the child row whose event_id matches Event.id. The child table may still contain helper columns such as id, event_id, notes, or sort fields; only the value field named by from.field is copied into the parent field.

In Excel, EventConditionEntry can look like this:

ABCDE
event_idtypequest_iditem_idcount
1QuestCompleted5002
2HasItem10012

From Options

The from object has these options:

OptionRequiredMeaning
tableyesChild table name. Sora scans this table for matching rows.
parent_keyyesField name on the parent table. Each parent row uses this field value for matching.
child_keyyesField name on the child table. A child row is selected when this value equals the parent key.
fieldnoField name on the child table. When present, Sora copies this field’s value instead of assembling a struct from the child row.
order_bynoField name on the child table. When present, matched child rows are sorted by this field in ascending order.

order_by is a field name, not an expression. There is no desc, multi-field ordering, filtering, or custom sort syntax. If order_by is omitted, matched rows keep the source table read order.

The order_by field must exist on the child table. It is usually an i32 ordering field such as sort_order, seq, or rank. Sorting is ascending.

Without from.field, the derived value type must be a struct, either list<struct<...>>, struct<...>, or optional<struct<...>>. Struct fields are copied from child table fields with the same names:

[[structs]]
name = "Reward"

[[structs.fields]]
name = "item_id"
type = "ref<Item.id>"

[[structs.fields]]
name = "count"
type = "i32"

Here Reward.item_id and Reward.count must both exist as compatible fields on QuestReward.

With from.field, the derived value type must be compatible with that child field. For example, type = "union<EventCondition>" can derive from a child field value whose type is also union<EventCondition>.

A derived field cannot also declare default. Its value comes from matched child rows.

Multiple Derived Fields from One Child Table

Several parent tables can derive fields from the same child table. This does not consume or move child rows. It reads the child table and copies matching values into each parent field.

For example, both Quest and QuestPreview can receive rewards from QuestReward:

[[tables]]
name = "Quest"
mode = "map"
key = "id"

[[tables.fields]]
name = "rewards"
type = "list<struct<Reward>>"
from = { table = "QuestReward", parent_key = "id", child_key = "quest_id", order_by = "sort_order" }

[[tables]]
name = "QuestPreview"
mode = "map"
key = "id"

[[tables.fields]]
name = "rewards"
type = "list<struct<Reward>>"
from = { table = "QuestReward", parent_key = "id", child_key = "quest_id", order_by = "sort_order" }

If both Quest.id = 1001 and QuestPreview.id = 1001 exist, both parent rows receive the reward list from QuestReward.quest_id = 1001. Sora does not mark the child row as already used by Quest, and it does not remove the row from QuestReward.