Skip to content

Store term localizations as separate rows #580

Draft
ryanmitchell wants to merge 3 commits into5.xfrom
fix/issue-578
Draft

Store term localizations as separate rows #580
ryanmitchell wants to merge 3 commits into5.xfrom
fix/issue-578

Conversation

@ryanmitchell
Copy link
Copy Markdown
Contributor

Previously, all locale data for a term was embedded in a single database row under a data->localizations JSON key. This made it impossible to query terms by site using a real WHERE site = ? clause, since translations stored inside a row's JSON are invisible to SQL.

This PR moves each localization to its own database row. The canonical (default-locale) row keeps origin = NULL, and each translation row stores origin = <canonical_id>.

This aligns with how entries already work.

Closes #578

ryanmitchell and others added 3 commits May 5, 2026 08:40
Replaces the previous approach of embedding localizations in the
canonical row's `data->localizations` JSON field. Each locale now
gets its own row (origin = null for canonical, origin = canonical_id
for translations), enabling real WHERE site = ? queries.

- Add migration to add nullable `origin` column to taxonomy_terms
- Add data migration to move existing JSON localizations to own rows
- TermQueryBuilder: drop $site property; add siteFilterApplied flag
  and applyOriginFilter() so default queries return one row per term;
  preload all locale rows in transform() to avoid N+1
- Term::fromModel() accepts preloaded sibling rows; builds locale data
  from per-row storage with fallback for legacy JSON localizations
- Term::makeModelFromContract() writes canonical row only (no localizations key)
- TermRepository::save() writes per-locale rows after the canonical model
- TermRepository::delete() removes all locale rows for a term
- ImportTaxonomies: saves locale rows after canonical on import
- ExportTaxonomies: reads canonical rows + sibling locale rows on export
- Update tests to reflect new per-row storage model

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TermQueryBuilder::transform() preloads locale rows and stores them in
Blink keyed by taxonomy::slug. Term::fromModel() reads from Blink when
available and falls back to a direct DB query otherwise, keeping the
method signature unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ryanmitchell
Copy link
Copy Markdown
Contributor Author

@duncanmcclean this is likely a breaking change given the underlying storage changes. what do you think?

@duncanmcclean
Copy link
Copy Markdown
Member

I like the idea. It would certainly make localizations easier to query.

However, as you say, it's likely a breaking change so it'll probably need to wait until the next major release, whenever that is (probably Q1, alongside v7) 😞.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TermQueryBuilder::where('site', '=', $handle) silently stores = as the site (3-arg form bypasses normalization)

2 participants