Tables
Tables are source-backed row collections. A table schema declares the table mode, source location, fields, and optional indexes.
Modes
| Mode | Shape | Typical Use |
|---|---|---|
map | Rows keyed by one field. | Items, quests, levels, buffs. |
list | Ordered rows without keyed lookup. | Drop entries, weighted pools, ordered steps. |
singleton | One row. | Global settings, tuning constants. |
[[tables]]
id = "item"
name = "Item"
mode = "map"
key = "id"
[[tables.fields]]
name = "id"
type = "i32"
For map tables, key names the table’s primary key field. Sora uses it for row uniqueness, generated lookup APIs, Excel template hints, and ref<Table.key> validation.
The table id is the stable schema identity used by view selection and table
aliases. It must remain unchanged when name changes. name is the canonical
source name used by data files and schema references; a view may expose a
different consumer-facing name without changing either the ID or source data.
Source
[tables.source]
format = "xlsx"
file = "Core.xlsx"
sheet = "Item"
format can be omitted when the project or command provides a default source format. file is resolved under the command’s --data-root during export and validation.
Built-in source formats are xlsx, csv, toml, json, and yaml. JSON and YAML table files are arrays of row objects:
[
{ "id": 1001, "name": "Iron Sword" },
{ "id": 1002, "name": "Health Potion" }
]
For JSON and YAML, file can also point to a directory. In that case Sora recursively reads every matching .json, .yaml, or .yml file as one row object, sorted by path.
Indexes
Indexes are extra lookup paths on a table. They are different from the key of a mode = "map" table:
| Concept | Purpose |
|---|---|
table key | The primary key. A map table uses it to keep rows unique and to generate the main get(id) lookup. |
[[tables.indexes]] | Additional lookup paths, such as lookup by name, grouping by type, or finding drops by stage. |
For example, an Item table can use id as its primary key:
[[tables]]
id = "item"
name = "Item"
mode = "map"
key = "id"
[[tables.fields]]
name = "id"
type = "i32"
[[tables.fields]]
name = "name"
type = "string"
[[tables.fields]]
name = "item_type"
type = "enum<ItemType>"
Add a unique index when another field should also identify at most one row:
[[tables.indexes]]
name = "by_name"
fields = ["name"]
unique = true
Example data:
| id | name | item_type |
|---|---|---|
| 1001 | Iron Sword | Weapon |
| 1002 | Wood Shield | Armor |
unique = true means name cannot repeat. Generated code for targets that support the index can expose a helper similar to get_by_name("Iron Sword"), returning one row or no row.
Use a non-unique index when a key can match many rows:
[[tables.indexes]]
name = "by_item_type"
fields = ["item_type"]
unique = false
Example data:
| id | name | item_type |
|---|---|---|
| 1001 | Iron Sword | Weapon |
| 1002 | Bronze Axe | Weapon |
| 2001 | Wood Shield | Armor |
unique = false means one key can match several rows. Generated code for targets that support the index can expose a helper similar to get_by_item_type(ItemType::Weapon), returning the matching rows.
fields is a list, so a unique index can also express combined uniqueness:
[[tables.indexes]]
name = "by_world_stage"
fields = ["world", "stage"]
unique = true
This requires each (world, stage) pair to be unique. For example, (1, 1) can appear once, while (1, 2) is a different key. Current generated lookup helpers mainly support single-field indexes on non-singleton tables; combined indexes are most useful for validation today.
Validation
Sora validates table rows after loading source data:
- non-optional fields must be present unless a default exists;
- key fields must be unique for map tables;
- enum values must be valid;
- references must point to existing rows;
- numeric ranges and length ranges must pass;
- parser output must match the declared field type.