Skip to content

fix(loader): restore early LIMIT for taxonomy listings on D1 (#3299) - #3300

Merged
ascorbic merged 2 commits into
emdash-cms:mainfrom
merrickma:bot/fix-3299
Sep 24, 2026
Merged

ascorbic merged 2 commits into
emdash-cms:mainfrom
merrickma:bot/fix-3299

Conversation

@merrickma

@merrickma merrickma commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Restores the early-LIMIT query plan for taxonomy-filtered listings on D1/SQLite, which was lost when migration 068_content_taxonomy_entry_groups (#2461) dropped the sort indexes 051_content_taxonomies_denorm (#1962) had created.

The picked CTE in buildTaxonomyPivotQuery changes CROSS JOIN → JOIN for SQLite: the planner can then drive from ec_*'s (deleted_at, <sort> DESC, id DESC) index, probe the pivot by primary key, and short-circuit at LIMIT, instead of looping over the whole collection per pivot row and temp-sorting.

Mechanical note: the change itself comes from the branch bot/fix-3299 (commit 8a3f0ea6a, authored by emdashbot from EmDash-Run ce45cf65-ead9-4d19-951a-ad9b4e1e11a6), which had not yet been turned into a PR — I mirrored the exact commit from my fork (authorship preserved) so it can get reviewed and land. h/t @emdashbot for the change.

Closes #3299

Type of change

  • Performance improvement

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes (run by the branch's CI; happy to re-run on request)
  • pnpm lint passes
  • pnpm test passes — the plan tests (loader-taxonomy-pivot-plan.test.ts) were updated by the branch to assert idx_ec_post_deleted_published_id is used and no temp B-tree
  • pnpm format has been run
  • I have added/updated tests for my changes (loader-taxonomy-pivot-plan.test.ts)
  • User-visible strings in the admin UI are wrapped for translation (not applicable — no UI strings)
  • I have added and reviewed the user-facing changeset (.changeset/eager-shirts-tell.md, emdash: patch)
  • New features link to an approved Discussion: not applicable
  • Screenshot: not applicable (non-UI change)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: change authored upstream by emdashbot (automated EmDash-Run ce45cf65-ead9-4d19-951a-ad9b4e1e11a6); PR opened mechanically via gh CLI

Screenshots / test output

Not applicable (no UI change). Production measurement on a stats-blind D1 deployment (single-locale site, 931 published posts, largest term = 907 assignments, emdash 0.38.0):

shape D1 rows_read for one listing
current (CROSS JOIN) 847,162
this fix (plain JOIN) 96
non-pivot EXISTS path 102

EXPLAIN before the fix shows USE TEMP B-TREE FOR ORDER BY; after, the picked CTE uses idx_ec_post_deleted_published_id with no temp B-tree — exactly what the updated tests assert. Full reproduction and measurement in #3299.

Downstream note: our site is currently pinned on an equivalent local patch; once this ships we'll drop that patch segment and re-test.

…cms#3299)

Change buildTaxonomyPivotQuery's picked CTE CROSS JOIN to JOIN on SQLite, preventing full nested loops after migration 068.

EmDash-Run: ce45cf65-ead9-4d19-951a-ad9b4e1e11a6
@changeset-bot

changeset-bot Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fa021d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/plugin-test Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://bot-fix-3299.try.emdashcms.com, https://bot-fix-3299-emdash-playground.emdash-cms.workers.dev (commit 8a3f0ea)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://597b4f74.try.emdashcms.com, https://597b4f74-emdash-playground.emdash-cms.workers.dev 8a3f0ea 2026-09-23T06:40:22.423Z Visit the dashboard ↗

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right fix for #3299. Migration 068 collapsed content_taxonomies to one row per content group/term group and dropped the denormalized sort indexes that #1834 relied on. With CROSS JOIN pinning the pivot as the outer table on SQLite, the indexed-sort path has no sort-ordered pivot index to short-circuit from, and the content table’s own (deleted_at, <sort> DESC, id DESC) index is unusable—so D1 falls back to a full nested loop plus temp B-tree. Changing to JOIN lets the SQLite planner reorder the CTE and drive from ec_* for published_at/created_at sorts, which is exactly what the measurements in the PR description show.

I checked the changed code, the surrounding loader logic, the existing correctness tests in loader-taxonomy-pivot.test.ts, and the migrations involved (051/068/033/034). The change is backwards compatible (no schema or API surface changes), locale/status/deletion filters remain intact, the pivotContentJoin value is still safely emitted via Kysely sql templates, and the regression tests target the right SQLite plan shape.

One design suggestion: the JOIN relaxation is only needed in the indexed-sort branch. The temp-sort branch (updated_at, multi-field sorts) has no sort index on ec_* and relies on the planner still driving from the pivot. On SQLite that was the whole reason for the original CROSS JOIN pin. I’d prefer a narrower condition that keeps the pin for the non-indexed path while allowing reordering where the fix is needed. The updated-at plan test could also assert the specific pivot index again if the pin is retained, rather than only checking that content_taxonomies appears.

Nothing here is blocking, and the measured impact justifies the fix.

Comment on lines 948 to +951
: sql``;

const firstGroupCond = pivotGroupCondition("ct.taxonomy_id", firstGroups);
const pivotContentJoin = isPostgres(db) ? sql`JOIN` : sql`CROSS JOIN`;
// A plain JOIN lets SQLite reorder the `picked` CTE. For indexed sorts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The JOIN relaxation is only required for the indexed-sort branch (published_at/created_at). The temp-sort path (updated_at, multi-field sorts) does not have a content-table sort index to drive from and still depends on the planner driving from content_taxonomies. That was the original reason this join was pinned with CROSS JOIN on SQLite. Consider keeping the pin for that branch so the change is scoped to the regression being fixed:

Suggested change
: sql``;
const firstGroupCond = pivotGroupCondition("ct.taxonomy_id", firstGroups);
const pivotContentJoin = isPostgres(db) ? sql`JOIN` : sql`CROSS JOIN`;
// A plain JOIN lets SQLite reorder the `picked` CTE. For indexed sorts
const pivotContentJoin = isIndexedSort || isPostgres(db) ? sql`JOIN` : sql`CROSS JOIN`;

If you keep the uniform JOIN, the updated-at test only checks that content_taxonomies appears in the plan rather than asserting idx_content_taxonomies_group_lookup; that weakens the regression guard for a path that was previously explicitly pinned.

@emdashbot emdashbot Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Sep 23, 2026
@ascorbic

Copy link
Copy Markdown
Member

@merrickma thanks. for future reference, you just need to reply to the bot to confirm it worked and it'll open the PR

@pkg-pr-new

pkg-pr-new Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@3300

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@3300

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@3300

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@3300

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@3300

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@3300

emdash

npm i https://pkg.pr.new/emdash@3300

create-emdash

npm i https://pkg.pr.new/create-emdash@3300

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@3300

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@3300

@emdash-cms/plugin-test

npm i https://pkg.pr.new/@emdash-cms/plugin-test@3300

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@3300

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@3300

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@3300

@emdash-cms/registry-loader

npm i https://pkg.pr.new/@emdash-cms/registry-loader@3300

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/@emdash-cms/registry-moderation@3300

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@3300

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@3300

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@3300

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@3300

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@3300

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@3300

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@3300

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@3300

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@3300

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@3300

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@3300

commit: fa021d7

@github-actions github-actions Bot added review/needs-rereview New commits since the last review overlap and removed review/awaiting-author Reviewed; waiting on the author to respond labels Sep 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

D1 full nested loop on taxonomy-filtered listings after 068 dropped the pivot sort indexes

2 participants