Skip to content

fix(core): batch taxonomy term counts under D1's compound-SELECT limit - #2331

Open
MA2153 wants to merge 2 commits into
emdash-cms:mainfrom
MA2153:fix/taxonomy-term-counts-compound-select
Open

fix(core): batch taxonomy term counts under D1's compound-SELECT limit#2331
MA2153 wants to merge 2 commits into
emdash-cms:mainfrom
MA2153:fix/taxonomy-term-counts-compound-select

Conversation

@MA2153

@MA2153 MA2153 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

fetchVisibleTermCounts built one UNION ALL branch per declared collection. Cloudflare D1 sets SQLITE_LIMIT_COMPOUND_SELECT to 5 — SQLite's own default is 500 — so a taxonomy declaring six or more collections produced SQL the backend rejected outright. Because the counts decorate the admin term list, the failure took the whole list down: GET /_emdash/api/taxonomies/:name/terms returned 500 and the admin rendered an empty list for a taxonomy whose terms were perfectly intact.

The ceiling is 5, measured against a live D1 rather than inferred: with the same query shape and only the branch count varying, five UNION ALL branches compile and six fail with too many terms in compound SELECT. That also settles #895, where the dashboard hit this at 9 arms and was fixed by fan-out without anyone bisecting the real threshold.

The fix batches collections into groups of SQL_COMPOUND_SELECT_LIMIT (new constant next to SQL_BATCH_SIZE in utils/chunks.ts), runs the batches concurrently, and sums the resulting maps — per-collection sums are commutative, so partitioning cannot change a total. The missing-ec_*-table fallback moves per batch, so one absent table only degrades its own batch instead of the whole computation.

Chunk size is 5, not a more conservative 4, on purpose: a taxonomy at or below the ceiling still issues exactly one query, so nothing on the logged-out render path regresses. 4 would push every 5-collection taxonomy from one query to two. The perf fixture's taxonomies declare one collection each, so the query-count snapshots are unchanged.

handleTermList's bare catch now logs the original error. Diagnosing this required reconstructing the generated SQL by hand, because nothing anywhere recorded why the list failed.

Closes #2330

Notes on the issue's other two suggestions

Deliberately not included, happy to be overruled:

  • Widening the fallback's isMissingTableError predicate. With the ceiling gone, the only errors a wider predicate would newly swallow are genuine ones.
  • Degrading handleTermList to counts-free terms on a count failure. It trades a loud 500 for silently wrong-looking admin data, and the failure that motivated it no longer exists. That reads like a maintainer's call about how much decoration is allowed to fail, not something to slip into a bug fix.

Testing

TDD: the regression test failed with exactly the reported error at runCounts before the fix.

better-sqlite3 uses SQLite's upstream default of 500 and offers no way to lower it, so a query shape D1 rejects runs happily in tests. setupTestDatabaseWithCompoundSelectLimit() imposes the ceiling when a statement is prepared — where SQLite raises it too — with D1's error text.

Two cases:

  • counts aggregate across LIMIT + 1 collections, and handleTermList returns them (the reported symptom);
  • a missing ec_* table beyond the first batch is still skipped. Mutation-checked by restricting the fallback to the first batch — the test fails as intended.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change) — full packages/core unit + integration suite, 4993 passed
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable) — n/a, no admin UI strings; the added log line is server-side and English-only by convention
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion — n/a, bug fix

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 5 (Claude Code)

Screenshots / test output

Before the fix, with the D1 ceiling imposed on the test database:

FAIL  tests/unit/taxonomies/term-counts.test.ts > visible term counts past the
      compound-SELECT ceiling > aggregates every declared collection when
      there are more than one statement can carry
Error: too many terms in compound SELECT: SQLITE_ERROR
 ❯ runCounts src/taxonomies/term-counts.ts:69:17
 ❯ Module.fetchVisibleTermCounts src/taxonomies/term-counts.ts:104:10

After:

Test Files  389 passed | 1 skipped (390)
     Tests  4993 passed | 3 skipped (4996)

Review response

Comment discipline — #2330 in the test file (40fae20). Fixed, and the review is right on both counts. The JSDoc block above the new describe is deleted outright rather than reworded: it was narrative about how the bug presented, which is exactly what the commit message and this description are for. The describe name is now visible term counts past the compound-SELECT ceiling — it already said what the block covers without the number. Nothing else in the diff carries an issue reference; the #581 in term-counts.ts:2 is pre-existing and untouched.

Targeted suite re-run after the edit: tests/unit/taxonomies/term-counts.test.ts — 10 passed.

🤖 Generated with Claude Code

fetchVisibleTermCounts built one UNION ALL branch per declared collection.
Cloudflare D1 sets SQLITE_LIMIT_COMPOUND_SELECT to 5 (SQLite's own default is
500), so a taxonomy declaring six or more collections produced SQL the backend
rejected outright. The counts decorate the admin term list, so the whole list
500'd and rendered empty while the terms themselves were intact.

Batch the branches into groups of SQL_COMPOUND_SELECT_LIMIT and sum the
resulting maps. Taxonomies at or below the ceiling still issue exactly one
query, so nothing on the logged-out render path regresses. The missing-ec_*-
table fallback moves per batch so an absent table only degrades its own batch.

The ceiling was measured against a live D1: five UNION ALL branches compile,
six fail with "too many terms in compound SELECT".

better-sqlite3 offers no way to lower the limit, so the regression test imposes
it at prepare() — where SQLite raises it too — with D1's error text.

handleTermList's bare catch now logs the original error; reconstructing the
generated SQL by hand was the only way to diagnose this.

Closes emdash-cms#2330

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 40fae20

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

This PR includes changesets to release 17 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash 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

@pkg-pr-new

pkg-pr-new Bot commented Aug 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 40fae20

@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 PR correctly addresses the D1 compound-SELECT ceiling: batching UNION ALL branches at the measured limit of 5 keeps the single-query fast path for taxonomies with ≤5 collections while avoiding the D1 error for larger ones, and moving the missing-table fallback inside each batch preserves the graceful-degradation behavior. I read the changed implementation, the new test harness, callers (handleTermList/handleTermGet and the request-cached getVisibleTermCounts), SQL construction, and checked against AGENTS.md conventions.

The logic is sound, the regression tests cover the reported failure (six collections) and the missing-table-across-batches case, and the changeset is user-facing and properly scoped. The only blocker-ish issue is a comment-discipline violation: the new test block references the closed issue #2330 in both a JSDoc comment and the describe() string, which AGENTS.md explicitly forbids. Everything else is clean.

Comment thread packages/core/tests/unit/taxonomies/term-counts.test.ts Outdated
@github-actions

github-actions Bot commented Aug 3, 2026

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.

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Aug 3, 2026
Comments are evergreen; the compound-SELECT background belongs in the
commit message and PR description, not in the test file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Aug 3, 2026
@MA2153 MA2153 added the bot:review Trigger an emdashbot code review on this PR label Aug 3, 2026

@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.

The approach is sound: batching a taxonomy's collections at D1's measured compound-SELECT limit of 5 keeps the single-query fast path for the common ≤5-collection case, while letting larger taxonomies survive. Moving the missing-table fallback per batch preserves the existing graceful-degradation behavior without one absent collection poisoning the whole count. This re-read checks the author addressed the prior review note and re-scrutinizes the changed code against AGENTS.md.

What's fixed versus the last review:

  • The #2330 issue reference is gone: the JSDoc block above the new describe was deleted, and the describe string now reads visible term counts past the compound-SELECT ceiling without an issue number.

Fresh checks on the current diff:

  • term-counts.ts chunks by SQL_COMPOUND_SELECT_LIMIT (5) and runs batches concurrently; per-group sums are commutative, so partitioning cannot change totals.
  • The missing-ec_*-table fallback is now scoped to each batch, verified by the new regression test for the missing table falling beyond the first batch.
  • handleTermList now logs the original server-side error before returning a safe envelope; this is diagnostics only and does not leak to clients.
  • SQL construction still uses Kysely's tagged templates and sql.ref(); no raw interpolation, no new identifier validation gaps.
  • The public render path calls fetchVisibleTermCounts through the request-cached getVisibleTermCounts, which already keys on the sorted collection scope; the only hot-path change is that an invalid case (>5 collections) now works instead of 500ing.
  • Changeset is present, present-tense, and user-facing.
  • No new comments reference issues/PRs/review threads; the remaining #581 references are pre-existing and were not introduced by this change.

I found nothing else that needs fixing.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 3, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Aug 4, 2026
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.

Taxonomy term list 500s when a taxonomy declares more collections than the backend's compound-SELECT limit

1 participant