fix(core): match localized taxonomy term slugs in collection where filter (#1480)#1554
Conversation
…lter (emdash-cms#1480) The loader's taxonomy EXISTS filter joined taxonomies on `t.id = content_taxonomies.taxonomy_id`, but since migration 036 the junction stores the term's translation_group, not a row id. The two coincide only for the default-locale anchor row, so filtering a non-default locale by its localized term slug matched nothing, and the subquery ignored the query locale entirely. Join on `t.translation_group` and scope `t.locale` to the query locale when set, mirroring the canonical pattern in taxonomies/index.ts and the base row query's localeFilter. Update three loader test fixtures that seeded taxonomies rows without translation_group (an unrealistic shape the old t.id join tolerated; migration 036 always backfills it = id). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 6bb429f The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Approach: This is the right fix for the right problem. Since migration 036, content_taxonomies.taxonomy_id stores a term's translation_group (not a row id), so the loader's old t.id = ct.taxonomy_id join only ever landed on the default-locale anchor row, and the subquery carried no t.locale predicate — so filtering a non-default locale by its localized term slug matched nothing and the locale was ignored entirely. The fix keys the join on t.translation_group and adds a conditional t.locale = <locale> filter, which exactly mirrors the canonical pattern already in taxonomies/index.ts (getEntryTerms) and repositories/taxonomy.ts (verified: same join column, same conditional locale scoping). No migration is needed because 036 backfills translation_group = id, and default-locale-only sites are genuinely unaffected. It's a scoped, backwards-compatible bug fix — no Discussion prerequisite applies.
Implementation (what I checked):
- SQL safety:
type,f.name, each slug, andlocaleare all bound via thesqltagged template;tableNamegoes through validatedsql.ref(getTableNamecallsvalidateIdentifier). No interpolation. - Consistency: the conditional
${locale ? sql\AND t.locale = ${locale}` : sql``}uses the same idiom as the base query'slocaleFilterin the same function, so the taxonomy filter scopes locale exactly when the base row query does. The loader usesfilter?.localedirectly (notresolveLocale), but that's the pre-existing base-query behavior — the PR is consistent with it, not a new gap. - Indexes: join column
taxonomies.translation_groupis indexed (idx_taxonomies_translation_group),content_taxonomies.taxonomy_idhasidx_content_taxonomies_term, and the(name, slug, locale)unique constraint covers the filter predicates. EXISTS is a semi-join with no row fan-out. - Edge cases: locale-less queries still resolve (predicate is conditional, matching the locale-less base query); a term whose group has no row in the query locale is correctly omitted (consistent with the documented "omitted" behavior); OR-within-taxonomy is locale-scoped;
translation_groupis never NULL for properly created terms (036 backfill +TaxonomyRepository.create), and orphaned pivots are purged on delete. - No other query path in the tree still uses the old
t.id = ct.taxonomy_idjoin (the only remaining occurrence is inside migration 036's remap logic, correct in context). The single-entry loader path does no taxonomy filtering.
Tests: The new #1480 regression test reproduces the bug (FR slug match returned 0 before the fix, 1 after), asserts a FR-site/EN-slug query returns 0, and guards the locale-less path. It runs on both dialects via describeEachDialect. The three fixture edits (seeding translation_group = id) are required for the new join to match — the old fixtures seeded terms with NULL translation_group, an unrealistic shape that real migrations never produce and that masked the bug. Migration tests that insert terms without translation_group don't exercise the loader join, so they're unaffected.
Changeset: present, emdash: patch, leads with a user-facing "Fixes…".
Headline: the code is clean and I'd sign off. The only thing worth a note is changeset prose that leans on implementation mechanics more than AGENTS.md asks for — a minor suggestion, not a blocker.
| "emdash": patch | ||
| --- | ||
|
|
||
| Fixes collection `where` taxonomy filters not matching localized term slugs (#1480). The loader's taxonomy filter joined `taxonomies` on `t.id = content_taxonomies.taxonomy_id`, but since the i18n migration the junction stores the term's `translation_group`, not a row id — the two coincide only for the default-locale term. As a result, filtering a non-default locale by its localized term slug matched nothing, and the filter silently ignored the query locale entirely. The join now keys on `translation_group` and scopes to the query locale (when one is set), matching the pattern used by every other taxonomy lookup, so localized term filtering works and a term slug resolves in the active locale. |
There was a problem hiding this comment.
[suggestion] Per AGENTS.md, a changeset is release notes a user reads while upgrading: lead with a present-tense verb, describe the observable effect, and leave out internal mechanics (file names, refactors, how you implemented it) — one sentence is often enough. The first sentence here does that well, but the rest of the entry is implementation detail (t.id = content_taxonomies.taxonomy_id, translation_group, "the junction stores…", "the join now keys on…"). A user running the new version only needs to know the behavior changed, not the join keys.
| Fixes collection `where` taxonomy filters not matching localized term slugs (#1480). The loader's taxonomy filter joined `taxonomies` on `t.id = content_taxonomies.taxonomy_id`, but since the i18n migration the junction stores the term's `translation_group`, not a row id — the two coincide only for the default-locale term. As a result, filtering a non-default locale by its localized term slug matched nothing, and the filter silently ignored the query locale entirely. The join now keys on `translation_group` and scopes to the query locale (when one is set), matching the pattern used by every other taxonomy lookup, so localized term filtering works and a term slug resolves in the active locale. | |
| Fixes collection `where` taxonomy filters matching nothing when filtering a non-default locale by its localized term slug. Term slugs now resolve in the active query locale (#1480). |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
What does this PR do?
The
emdashLoadercollectionwheretaxonomy filter joinedtaxonomiesont.id = content_taxonomies.taxonomy_id. Since migration 036,content_taxonomies.taxonomy_idstores the term'stranslation_group, not a row id — the two coincide only for the default-locale anchor row. As a result:t.localepredicate, so it ignored the query locale entirely even though the base row query already filters locale.This changes the join to key on
t.translation_groupand adds a conditionalt.locale = <query locale>filter (only when a locale is set), mirroring the canonical pattern already used intaxonomies/index.tsandrepositories/taxonomy.ts. No migration needed; default-locale-only sites are unaffected (translation_group = idthere).Also fixes three loader test fixtures that seeded
taxonomiesrows withouttranslation_group— an unrealistic shape the oldt.idjoin tolerated, which masked the bug (migration 036 always backfillstranslation_group = id).Closes #1480
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (targeted: loader + taxonomy suites, both dialects viadescribeEachDialect)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
New regression test asserts: FR-locale filter by the FR slug matches (0 before the fix), FR slug does not match the EN slug, and a locale-less query still resolves the default-locale slug.
loader-taxonomy-filter+loader-field-filters+loader-byline-filter+ taxonomy integration suites green.