Skip to content

References⚓︎

Use reference() when one record in a table points to another record from the same parsed table.

This is common in BDD setup data. CMS pages have parents, articles mention related content, tasks depend on earlier tasks, accounts may have managers, and orders may refer to existing rows in the same authored example. In plain table text those relationships are written as IDs. In test code, it is more useful when those IDs have been resolved to the actual parsed records.

A content table with local relationships
Feature: CMS content relationships

  Scenario: Build linked content from one table
    Given the content items:
      | IDs      | ROOT      | A-1          | P-1         |
      | Headline | Home      | Launch story | Reader poll |
      | Parent   |           | ROOT         | ROOT        |
      | Related  | A-1, P-1 | P-1          |             |
    Then parent and related content references resolve

Talika references are deliberately local. They do not look in a database, a global registry, another scenario, or a previously parsed table. They only resolve against the records produced by the current parse call.

Think local graph, not external lookup

A reference cell contains a key. Talika resolves that key to another record from the same parsed result, then replaces the reference field with the resolved record object.

Declare an ID and a Reference⚓︎

References need a target field. In a ColumnTable, that usually starts with an id_field(...) because each item column needs a stable identity.

A content schema with references
from talika import (
    ColumnTable,
    RowTable,
    TableError,
    field,
    id_field,
    integer,
    reference,
)


class ContentTable(ColumnTable):
    id = id_field("IDs")
    headline = field("Headline", required=True)
    parent = reference("Parent")
    related = reference("Related", many=True)

This schema declares two reference fields:

  • parent is a single reference to one content item
  • related is a many-reference field containing several content IDs

Both references use the default target, id, because the schema has an attribute named id. The table cells still contain text such as ROOT or A-1; the parsed records receive object references.

The authored content table
content_table = [
    ["IDs", "ROOT", "A-1", "P-1"],
    ["Headline", "Home", "Launch story", "Reader poll"],
    ["Parent", "", "ROOT", "ROOT"],
    ["Related", "A-1, P-1", "P-1", ""],
]

Reference resolution happens after all records have been parsed and before record validation runs. That timing matters because every target record must exist before links can be connected.

Reference labels are ordinary fields

reference("Parent") still declares a field. It can be required, can have aliases, and participates in source-aware diagnostics. The difference is that the final value becomes a record object instead of the raw key.

Resolve One Reference⚓︎

For a single reference, the authored cell contains one key.

Resolving parent content
root, article, poll = ContentTable.parse(content_table)

assert root.parent is None
assert article.parent is root
assert poll.parent is root
assert article.parent.headline == "Home"
Single reference result
>> article.id
'A-1'

>> article.parent.id
'ROOT'

>> article.parent.headline
'Home'

The root item has an empty Parent cell, so root.parent becomes None. The article and poll both point to ROOT, so their parent fields become the same ContentTable record object as root.

This is usually what test setup wants. The table author writes small readable keys, while the test code can navigate records directly.

Empty optional references become None

An empty single-reference cell means "no linked record" unless the field is declared with required=True. It does not try to resolve an empty string as an ID.

Resolve Many References⚓︎

Set many=True when a cell can contain several keys.

Resolving related content
root, article, poll = ContentTable.parse(content_table)

assert root.related == [article, poll]
assert article.related == [poll]
assert poll.related == []
Many reference result
>> [item.id for item in root.related]
['A-1', 'P-1']

>> [item.id for item in article.related]
['P-1']

>> poll.related
[]

By default, many references split the cell on commas and trim whitespace around each key. The authored value A-1, P-1 becomes two lookup keys: A-1 and P-1.

An empty many-reference cell becomes an empty list. That lets the caller iterate over record.related without checking for None.

Use a separator that cannot appear in IDs

The default separator is ",". If your IDs can contain commas, choose a different separator when declaring the reference. An empty separator is not allowed.

Use Typed IDs⚓︎

Reference keys are parsed with the target field's parser before lookup.

That means a table can use typed IDs without making the reference field repeat the parser. The target id_field(...) owns the key type, and references follow that contract.

A table with integer IDs
class TypedContentTable(ColumnTable):
    id = id_field("IDs", parser=integer())
    headline = field("Headline")
    parent = reference("Parent")
Parsing typed references
parent, child = TypedContentTable.parse(
    [
        ["IDs", "101", "102"],
        ["Headline", "Root", "Child"],
        ["Parent", "", "101"],
    ]
)
Typed reference result
>> parent.id
101

>> type(parent.id).__name__
'int'

>> child.parent is parent
True

The child table cell contains "101", but the target ID is parsed as the integer 101. Talika converts the reference key with the same target parser before looking it up.

Put key parsing on the target field

If every record ID should be an integer, parse the id_field. References to that ID will use the same conversion automatically.

Reference Another Target Field⚓︎

The default reference target is "id". Use target=... when the authored table should link by another unique field.

Referencing by slug
class SlugContentTable(ColumnTable):
    id = id_field("IDs")
    slug = field("Slug", required=True)
    parent = reference("Parent slug", target="slug")
Resolving by a non-ID target
home, child = SlugContentTable.parse(
    [
        ["IDs", "1", "2"],
        ["Slug", "home", "child"],
        ["Parent slug", "", "home"],
    ]
)

assert child.parent is home
assert child.parent.id == "1"

Here the Parent slug cell contains home, so Talika looks for a record whose parsed slug attribute is "home". The resolved value is still the full record object, not the slug string.

This is useful when the table author should read or write meaningful labels, but the parsed record still has a separate technical ID.

For a non-variant schema, a missing target is rejected when the schema class is created. A family with an explicit discriminator may register variants later, so Talika validates the complete family when parsing begins.

The target must be unique

A reference target is an index. If two records have the same target value, Talika cannot know which record the authored key means, so parsing fails.

Target values must also be hashable. For variant families, a target may be declared on the base or exposed by selected variants. If several variants declare it, they must reuse the same parser object (or all omit a parser). Put the target on a common base or TableFields component when variants would otherwise disagree.

Validate After References Resolve⚓︎

Record validation runs after references are resolved. Validators can inspect linked records directly.

A validator that uses a resolved reference
class ValidatedContentTable(ColumnTable):
    id = id_field("IDs")
    parent = reference("Parent")

    def validate_record(self, context):
        if self.parent is self:
            raise TableError.from_cell(
                "Content cannot reference itself",
                self.source_for("parent"),
                schema=type(self),
            )

In this example, the parser first resolves parent. The validator then checks whether the resolved parent is the same object as the current record.

A self-reference
ValidatedContentTable.parse(
    [
        ["IDs", "ROOT"],
        ["Parent", "ROOT"],
    ]
)
Validation diagnostic
Content cannot reference itself (code=table_error, schema=ValidatedContentTable, row=2, column=2, value='ROOT')

The validator uses self.source_for("parent") because the fix belongs to the Parent cell. That gives the table author a precise source location instead of a vague record-level failure.

Validate relationships after resolution

If a rule needs to compare records, let reference() resolve the link first. The validator can then work with objects instead of raw ID strings.

Diagnose Missing References⚓︎

When a key cannot be found, Talika points at the reference cell where the bad key was written.

A reference to a missing ID
ContentTable.parse(
    [
        ["IDs", "ROOT", "A-1"],
        ["Headline", "Home", "Article"],
        ["Parent", "", "MISSING"],
        ["Related", "", ""],
    ]
)
Missing reference diagnostic
Reference target 'MISSING' was not found (code=reference_failed, schema=ContentTable, field='Parent', row=3, column=3, item_id='A-1', value='MISSING')

The diagnostic includes:

  • code=reference_failed
  • the schema and reference field
  • the source row and column of the bad key
  • the current item ID, when known
  • the authored key value

This is more helpful than reporting the target row, because the target row does not exist. The table author needs to fix the cell that contains MISSING.

Reference failures stop validation

Validators depend on links being trustworthy. If references cannot be resolved, Talika reports the reference problem before running dependent validation logic.

In collect mode, Talika checks reference fields in record order, declaration order, and key order. It can report several missing or conversion failures from one many-reference cell. A reference attribute is assigned only if every key for that field resolves. If any reference diagnostic exists, record/table validation and output conversion are skipped because they require complete links.

Diagnose Ambiguous Targets⚓︎

The target field must identify one record. If the target values are duplicated, Talika reports the duplicate target cell.

A schema that references slugs
class DuplicateSlugTable(ColumnTable):
    id = id_field("IDs")
    slug = field("Slug")
    parent = reference("Parent slug", target="slug")
Two records with the same slug
DuplicateSlugTable.parse(
    [
        ["IDs", "1", "2", "3"],
        ["Slug", "same", "same", "child"],
        ["Parent slug", "", "", "same"],
    ]
)
Duplicate target diagnostic
Reference target 'same' is not unique (code=reference_failed, schema=DuplicateSlugTable, field='Slug', row=2, column=3, value='same')

The bad reference cell is not the main issue here. The table cannot build a reliable slug index because two records both claim same. Talika points at the duplicate target value so the author can make the target field unique.

Do not reference non-unique vocabulary

If a field is useful for display but not guaranteed unique, do not use it as a reference target. Add a stable ID or a unique slug field instead.

Diagnose Bad Typed Keys⚓︎

When a target field has a parser, the reference key must be valid for that parser.

A reference key that cannot become an integer
TypedContentTable.parse(
    [
        ["IDs", "101", "102"],
        ["Headline", "Root", "Child"],
        ["Parent", "", "abc"],
    ]
)
Reference key conversion diagnostic
Reference key conversion failed: invalid literal for int() with base 10: 'abc' (code=reference_failed, schema=TypedContentTable, field='Parent', row=3, column=3, item_id=102, value='abc')

The error points at the reference cell, not the ID row. The ID parser is the rule being reused, but the invalid authored value is in Parent.

If that parser raises an ordinary exception while converting a reference key, Talika reports reference_failed and retains the cause. A deliberate TableError, TableErrors, or SchemaDefinitionError passes through unchanged.

Typed references use target parsing only for lookup

The reference field itself does not become an integer. It becomes the resolved record object. The key is converted only so Talika can find the matching target record.

References Also Work in Row Tables⚓︎

Column tables are common for CMS-style linked content, but references are not limited to column-shaped data. A row table can declare an id_field(...) and reference it by attribute name.

A row-shaped dependency table
class TaskTable(RowTable):
    key = id_field("key")
    title = field("title")
    depends_on = reference("depends on", target="key")
Resolving row-table dependencies
setup, login = TaskTable.parse(
    [
        ["key", "title", "depends on"],
        ["setup", "Create account", ""],
        ["login", "Sign in", "setup"],
    ]
)

assert setup.depends_on is None
assert login.depends_on is setup

The same local-resolution rules apply. Talika parses every row into a record, builds an index from key, and replaces depends_on with the referenced task record.

Use row-shaped references when each row is naturally one object and the table reader benefits from scanning relationships across rows.

Keep reference tables readable

References are easiest to maintain when keys are short, stable, and visible in the same table. If authors need to search elsewhere to understand a key, the table may need a clearer ID or shape.