Skip to content

Evolving Tables Safely⚓︎

Feature tables change. A team renames a label. A new optional field appears. A parser becomes stricter. Old scenarios still matter, but new scenarios should read better.

Table evolution is healthiest when compatibility is intentional.

Renaming language⚓︎

An old table might use Full name:

Older table wording
Given the users exist
  | Full name | age |
  | Mira Rao  | 34  |

A newer table might prefer name and add active:

Newer table wording
Given the users exist
  | name     | age | active |
  | Mira Rao | 34  | true   |

Both can be supported while the team migrates:

A contract that accepts old and new labels
from talika import RowTable, boolean, field


class UserTable(RowTable):
    name = field("name", aliases=("Full name",), required=True)
    age: int = field("age", required=True)
    active = field("active", parser=boolean(), default=True)

The important part is that the old wording is visible. It is accepted because the project chose to accept it, not because the parser ignored differences.

See field aliases for the concrete migration mechanism.

Compatibility should have a reason

Aliases are useful for migration, shared vocabulary, and external wording. They should not become a junk drawer for every spelling that ever appeared.

Adding fields⚓︎

Adding a required field breaks old tables immediately. Sometimes that is the right choice. Other times the field should start as optional with a default, so old scenarios remain valid while new scenarios can be more explicit.

Choose between those paths by asking whether an old table still describes a valid scenario without the new field. If it does not, make the field required and update the affected tables together. If omission still has a clear meaning, introduce the field as optional and document the fallback in the schema.

Defaults can hide change

A default is convenient, but it also makes omitted data look intentional. Use it when omission is genuinely acceptable, not just to avoid updating tables.

Preserve variant data during migration⚓︎

Variant tables sometimes contain a value that belonged to an older record shape. During a controlled migration, a project can preserve that known but inapplicable value instead of treating it as an active field:

Preserving known variant fields during migration
from talika import ColumnTable, TableFields, discriminator, field, id_field


class ArticleFields(TableFields):
    body = field("Body")


class PollFields(TableFields):
    options = field("Options")


class LegacyContentTable(ColumnTable):
    inapplicable_fields = "preserve"

    id = id_field("IDs")
    content_type = discriminator(
        "Type",
        variants={"Article": ArticleFields, "Poll": PollFields},
    )

Preserved values live in record.table_extras; they do not appear in record.as_dict() or become attributes on the selected variant. This policy only applies to labels declared elsewhere in the variant family. Completely unknown labels still follow the schema's unknown_fields policy.

Use preservation as a short-lived compatibility strategy while authors clean up old tables. The inapplicable-fields guide shows how to inspect and eventually remove the preserved values.

CI makes evolution safer⚓︎

When table rules are explicit, a checker can validate feature files before the scenario runs. That gives teams earlier feedback when a label changed, a parser became stricter, or a compatibility alias was removed.

The static-checking guide shows how to check feature tables from the CLI.

Evolve in public

Feature tables are shared language. When you change that language, make the transition visible in the contract and easy to detect in CI.