Skip to content

Column Group Expansion⚓︎

Column group expansion is for column-shaped tables where one authored column stands for several logical records.

That sounds abstract, but the table shape is common in feature files. Authors often want to say "IDs 1 through 3 are articles with the same headline" without copying the same values three times.

A compact grouped content table
Feature: CMS grouped content

  Scenario: One compact column describes several content records
    Given the content table:
      | IDs      | 1..3      | 4        |
      | Type     | 3:Article | Poll     |
      | Headline | Shared    | Vote now |
    Then the grouped columns become ordinary content records

Talika can expand that compact authoring shape before the ColumnTable schema parses it. After expansion, the schema sees a normal column table with one logical item column per ID.

Think in source groups

The first row contains the group key. Each following row contains one value for that source group. The range rule expands the key cell, and the repeat rule expands the aligned value cells to the same length.

Configure the Expander⚓︎

Attach ColumnGroupExpander as a table transformer on a ColumnTable.

A grouped content schema
from talika import (
    ColumnGroupExpander,
    ColumnTable,
    NumericRange,
    ParseContext,
    PrefixRepeat,
    TableData,
    field,
    id_field,
)


class ContentTable(ColumnTable):
    table_transformer = ColumnGroupExpander(
        key_row="IDs",
        range_rule=NumericRange(".."),
        repeat_rule=PrefixRepeat(":"),
    )

    id = id_field("IDs")
    content_type = field("Type")
    headline = field("Headline")


compact_content = [
    ["IDs", "1..3", "4"],
    ["Type", "3:Article", "Poll"],
    ["Headline", "Shared", "Vote now"],
]

items = ContentTable.parse(compact_content)

key_row="IDs" tells the expander which row is allowed to define groups. NumericRange("..") turns 1..3 into 1, 2, and 3. PrefixRepeat(":") turns 3:Article into three Article cells.

Records produced from grouped columns
>> [item.as_dict() for item in items]
[
    {'id': '1', 'content_type': 'Article', 'headline': 'Shared'},
    {'id': '2', 'content_type': 'Article', 'headline': 'Shared'},
    {'id': '3', 'content_type': 'Article', 'headline': 'Shared'},
    {'id': '4', 'content_type': 'Poll', 'headline': 'Vote now'},
]

The expanded table is the logical shape the schema parses:

Expanded logical table
>> ContentTable.table_transformer.transform(
...     TableData.from_rows(compact_content),
...     ParseContext(),
...     schema=ContentTable,
... ).to_rows()
[
    ['IDs', '1', '2', '3', '4'],
    ['Type', 'Article', 'Article', 'Article', 'Poll'],
    ['Headline', 'Shared', 'Shared', 'Shared', 'Vote now'],
]

Expansion happens before field parsing

Field labels, required fields, parsers, defaults, validators, and output conversion run after the grouped table has become a normal logical table.

Source Cells Stay Useful⚓︎

Expanded cells still point back to the compact cell that created them.

Source metadata after expansion
>> items[2].source_for("content_type")
TableCell(value='Article', source_row=2, source_column=2, source_value='3:Article')

>> items[1].source_for("headline")
TableCell(value='Shared', source_row=3, source_column=2, source_value='Shared')

The third content record gets Article from the original 3:Article cell. Both Shared headlines for IDs 1, 2, and 3 point back to the single Shared cell.

This is why group expansion can be used in real test suites without making diagnostics vague. The test gets simple records, while the failure still points to the authored compact table.

Use Built-In Range and Repeat Rules⚓︎

Range rules work on the key row:

  • NumericRange("..") supports values such as 1..3
  • AlphabeticRange("-") supports values such as A-C

NumericRange currently allows at most 10,000 generated keys from one compact cell. Exactly 10,000 is allowed; a larger range fails before allocation with expansion_limit at the compact source cell. This is a private stabilization guard rather than a configurable public limits API.

Repeat rules work on the value rows:

  • PrefixRepeat(":") supports values such as 3:Article
  • SuffixRepeat(" x") supports values such as Article x3

Values that do not use the configured syntax are treated as literals. For example, 4 is one ID, and Poll is copied once because it belongs to a single-key group.

Alphabetic keys with suffix repeats
from talika import AlphabeticRange, ColumnGroupExpander, ColumnTable, SuffixRepeat
from talika import field, id_field


class LetterContentTable(ColumnTable):
    table_transformer = ColumnGroupExpander(
        key_row="IDs",
        range_rule=AlphabeticRange("-"),
        repeat_rule=SuffixRepeat(" x"),
    )

    id = id_field("IDs")
    content_type = field("Type")


letter_rows = [
    ["IDs", "A-C"],
    ["Type", "Article x3"],
]

letter_items = LetterContentTable.parse(letter_rows)
Alphabetic expansion result
>> [item.as_dict() for item in letter_items]
[
    {'id': 'A', 'content_type': 'Article'},
    {'id': 'B', 'content_type': 'Article'},
    {'id': 'C', 'content_type': 'Article'},
]

Recognized range syntax must be valid

For range rules, a value that contains the configured separator is treated as range syntax. 3..1 is not a literal; it is a descending numeric range, so it fails. Repeat rules are a little more forgiving: a value such as News: Europe stays literal with PrefixRepeat(":") because the prefix is not a repeat count.

Write Custom Rules⚓︎

Use custom rules when your project has its own group language. Range rules implement expand(cell, context) and return key cells. Repeat rules implement expand(cell, expected_count, context) and return exactly that many value cells.

Those two shapes are the public RangeRule and RepeatRule protocols. They are structural protocols, so custom rule classes do not need to inherit from a Talika base class. The important part is the method signature and returning TableCell objects.

Custom group syntax with parse context
from talika import ColumnGroupExpander, ParseContext, TableData


class LabelRange:
    def expand(self, cell, context):
        labels = context.user_data["labels"][cell.value]
        return [cell.with_value(label) for label in labels]


class CopyWithIndex:
    def expand(self, cell, expected_count, context):
        return [
            cell.with_value(f"{cell.value} {index}")
            for index in range(1, expected_count + 1)
        ]


custom_expander = ColumnGroupExpander(
    key_row="Slots",
    range_rule=LabelRange(),
    repeat_rule=CopyWithIndex(),
)

custom_table = TableData.from_rows(
    [
        ["Slots", "launch"],
        ["Headline", "Draft"],
    ]
)
custom_context = ParseContext.from_value(
    {"labels": {"launch": ["hero", "sidebar"]}}
)

custom_result = custom_expander.transform(custom_table, custom_context)
Custom rule output
>> custom_result.to_rows()
[['Slots', 'hero', 'sidebar'], ['Headline', 'Draft 1', 'Draft 2']]

Custom rules should return TableCell objects, usually by calling cell.with_value(...). Returning plain strings would lose source coordinates, so Talika rejects it.

Keep rule objects small

Let the expander own the table mechanics. Let each rule own one syntax: how a key group is named, or how a value is spread across that group.

Understand Expansion Errors⚓︎

When a repeat count does not match the key range, the error points to the value cell with the incorrect count:

A repeat count that does not match its key group
ContentTable.parse(
    [
        ["IDs", "1..3"],
        ["Type", "2:Article"],
    ]
)
Repeat count diagnostic
Repeat expansion failed: Repeat count 2 does not match group size 3 (code=table_error, schema=ContentTable, row=2, column=2, value='2:Article')

If the key row is wrong, the error points to the first cell:

Wrong key row diagnostic
Expected key row 'IDs' (code=table_error, schema=ContentTable, row=1, column=1, value='Keys')

If a range is recognized but invalid, the error points to the key cell:

Invalid range diagnostic
Range expansion failed: Numeric range must be ascending (code=table_error, schema=ContentTable, row=1, column=2, value='3..1')

A practical rule of thumb

Use group expansion when compact authoring would remove noisy repetition from a column table. If the compact syntax makes the feature harder to review, prefer writing the ordinary expanded table directly.