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.
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.
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:
parentis a single reference to one content itemrelatedis 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.
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.
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"
>> 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.
root, article, poll = ContentTable.parse(content_table)
assert root.related == [article, poll]
assert article.related == [poll]
assert poll.related == []
>> [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.
class TypedContentTable(ColumnTable):
id = id_field("IDs", parser=integer())
headline = field("Headline")
parent = reference("Parent")
parent, child = TypedContentTable.parse(
[
["IDs", "101", "102"],
["Headline", "Root", "Child"],
["Parent", "", "101"],
]
)
>> 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.
class SlugContentTable(ColumnTable):
id = id_field("IDs")
slug = field("Slug", required=True)
parent = reference("Parent slug", target="slug")
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.
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.
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.
ContentTable.parse(
[
["IDs", "ROOT", "A-1"],
["Headline", "Home", "Article"],
["Parent", "", "MISSING"],
["Related", "", ""],
]
)
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.
class DuplicateSlugTable(ColumnTable):
id = id_field("IDs")
slug = field("Slug")
parent = reference("Parent slug", target="slug")
DuplicateSlugTable.parse(
[
["IDs", "1", "2", "3"],
["Slug", "same", "same", "child"],
["Parent slug", "", "", "same"],
]
)
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.
TypedContentTable.parse(
[
["IDs", "101", "102"],
["Headline", "Root", "Child"],
["Parent", "", "abc"],
]
)
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.
class TaskTable(RowTable):
key = id_field("key")
title = field("title")
depends_on = reference("depends on", target="key")
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.