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

Tables

Tables are source-backed row collections. A table schema declares the table mode, source location, fields, and optional indexes.

Modes

ModeShapeTypical Use
mapRows keyed by one field.Items, quests, levels, buffs.
listOrdered rows without keyed lookup.Drop entries, weighted pools, ordered steps.
singletonOne row.Global settings, tuning constants.
tables {
  Item {
    mode = "map"
    key = "id"
    fields { id = "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.

Source

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.

Split one XLSX table across sheets

An XLSX-backed table can combine several worksheets into one logical table. This is useful for data that is naturally partitioned for editing, such as one activity sheet per month:

An XLSX workbook belongs to exactly one table definition. Multiple tables cannot share the same source.file; use separate workbook files for separate schemas. Multiple sheets in that file are partitions of its one table, not separate table definitions.

source {
  format = "xlsx"
  file = "Activity.xlsx"
  sheets = ["2026-01", "2026-02", "2026-03"]
}

Every selected worksheet uses the same columns and parsers declared by the table schema. Sora concatenates their rows, then validates and exports the combined result as one table. Primary-key, unique-index, reference, and singleton checks therefore apply across all selected sheets, not separately per sheet.

Selectors may contain * (any sequence) and ? (one character):

source {
  format = "xlsx"
  file = "Activity.xlsx"
  sheets = ["2026-*", "special"]
}

Explicit sheet names keep declaration order. Matches from each wildcard selector are sorted by sheet name. Overlapping selectors include a worksheet only once. Loading fails when an explicit worksheet is missing or a wildcard matches nothing.

sheets is supported only for XLSX sources and cannot be combined with sheet. New template generation requires explicit sheet names because a wildcard cannot invent worksheets; wildcard selectors can be used with an existing workbook. Excel template generation and sync create missing explicitly named worksheets, and sync preserves rows in each selected worksheet. Row-level Studio data mutation currently rejects multi-sheet tables because merged rows do not retain worksheet ownership; schema editing, loading, validation, and export remain supported.

Indexes

Indexes are extra lookup paths on a table. They are different from the key of a mode = "map" table:

ConceptPurpose
table keyThe primary key. A map table uses it to keep rows unique and to generate the main get(id) lookup.
table indexesAdditional keyed 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 {
  Item {
    mode = "map"
    key = "id"
    fields {
      id = "i32"
      name = "string"
      item_type = "enum<ItemType>"
    }
  }
}

Add a unique index when another field should also identify at most one row:

indexes {
  by_name {
    fields = ["name"]
    unique = true
  }
}

Example data:

idnameitem_type
1001Iron SwordWeapon
1002Wood ShieldArmor

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:

indexes {
  by_item_type = ["item_type"]
}

Example data:

idnameitem_type
1001Iron SwordWeapon
1002Bronze AxeWeapon
2001Wood ShieldArmor

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:

indexes {
  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.