Skip to content

fix(search): skip row-count scans on cold search requests - #4760

Open
emdash-autofix[bot] wants to merge 2 commits into
mainfrom
emdash-bot/issue-4675
Open

emdash-autofix[bot] wants to merge 2 commits into
mainfrom
emdash-bot/issue-4675

Conversation

@emdash-autofix

@emdash-autofix emdash-autofix Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Removes content and FTS _docsize COUNT scans from the lazy health check on each new Workers isolate. Both search and autocomplete, including authenticated editors, now check for missing or empty indexes instead. The emptiness probe stops at the first FTS row; only if the index is empty does it probe live content using the existing deleted_at index. This keeps automatic recovery when an interrupted rebuild leaves a newly created table empty.

Full row-count verification remains the default for explicit FTSManager.verifyAndRepairAll() / verifyAndRepairIndex() calls. Non-empty, partially populated indexes are no longer automatically detected on requests: use the authenticated POST /_emdash/api/search/rebuild endpoint with X-EmDash-Request: 1 and JSON {"collection":"articles"} to repair them. This trade-off removes the unbounded healthy-index scans without introducing persistent health-marker state. The changeset documents recovery.

Adds a backwards-compatible checkRowCounts option and preserves singleton DB routing and single-flight error handling. Real SQLite tests cover repeated cold runtimes without COUNTs, empty-table recovery after an interrupted rebuild, truly empty collections without repeated rebuilds, missing-table recovery through both runtime and explicit verification, and explicit repair of non-empty drift. The cold-runtime regression failed before the fix with both reported COUNT statements.

Type of change

  • Bug fix
  • Feature (requires an accepted feature plan or RFC)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Your connection to this change

  • I use EmDash and ran into the problem this PR addresses
  • I maintain a tool, dependency, or service that EmDash uses, and this change concerns that integration
  • A Maintainer approved this specific change before I opened the PR

Assigned by danielmlr.

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • 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 (not applicable)
  • I have added and reviewed the user-facing changeset
  • New features link to their merged design PR (not applicable)
  • I have included screenshots below if this PR changes the UI (not applicable)

AI assistance disclosure

  • This PR includes AI-generated code or text — model/tool: cloudflare-ai-gateway:gpt-6.1-sol via the EmDash bot (Pi Durable)

Screenshots / test output

Screenshots: Not applicable; no UI changes.

  • Root pnpm build: passed with raised file-descriptor and Node heap limits (ulimit -n 65536, NODE_OPTIONS=--max-old-space-size=8192). Initial attempts hit sandbox resource limits.
  • pnpm --filter emdash typecheck: passed.
  • pnpm lint:quick: zero diagnostics.
  • pnpm format: passed.
  • pnpm --filter emdash exec vitest run tests/integration/search: passed (16 files; 70 tests passed, 4 skipped).

Full workspace typecheck and type-aware lint are left to CI.

Closes #4675

A Maintainer (@danielmlr) assigned this issue to the EmDash bot.

Replaced per-isolate search row-count verification with missing/empty-index checks while preserving explicit full verification and interrupted-rebuild recovery. Added real SQLite regression coverage that reproduced both reported COUNT queries before the fix. Verified search integration tests, root build with sandbox resource overrides, core typecheck, formatting, and lint:quick (zero diagnostics). Independent follow-up review found all findings addressed.

EmDash-Bot-Case: 1198246700-issue-6226228056
EmDash-Bot-Candidate: cand-1-34991f9790
@emdash-autofix emdash-autofix Bot added the bot:fix Assign this issue or pull request to the EmDash bot to fix label Nov 8, 2026
@emdash-autofix

emdash-autofix Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor Author

EmDash bot: Ready for human review

Plan

Clarified Admin and token requirements, added canonical recovery documentation, and made the query-capture assertion non-vacuous. Docs build, 7 repair tests, formatting, and quick lint pass; independent review found no blockers.

  • Read search health checks and existing tests
  • Implement a cheap request-path health check with regression coverage
  • Run focused checks and review the candidate
  • Clarify recovery permissions, document the procedure, and strengthen query capture
  • Verify, review, and publish the revision

Status

  • Required checks passed, the review bot approved the current head, and the head merges cleanly.

Next action: A maintainer will review and approve the pull request.

Details
Candidate cand-2-6f9fa078d4 (tree 83e624cbb7b2)
Published head b3dc7a51b5a8
Reviewer's latest review of head approved
Reporter confirmation not yet
Human approval required before merge

Required checks on the current head:

  • Typecheck: succeeded
  • Tests: succeeded
  • Smoke Tests: succeeded
  • Browser Tests: succeeded
  • Format: succeeded
  • CLAssistant: succeeded
  • Measure: succeeded

This status is generated from recorded evidence. It is not a review and contains no instructions.

@changeset-bot

changeset-bot Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b3dc7a5

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

@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Query-count snapshot changes

40 routes changed, total Δ +41 queries.

SQLite

Route Before After Δ
GET / (cold) 4 5 +1
GET / (warm) 4 5 +1
GET /category/development (cold) 9 10 +1
GET /category/development (warm) 8 9 +1
GET /contributors (cold) 4 5 +1
GET /contributors (warm) 4 5 +1
GET /contributors-naive (cold) 11 12 +1
GET /contributors-naive (warm) 11 12 +1
GET /posts (cold) 4 5 +1
GET /posts (warm) 4 5 +1
GET /posts/building-for-the-long-term (cold) 13 14 +1
GET /posts/building-for-the-long-term (warm) 12 13 +1
GET /related (cold) 7 8 +1
GET /related (warm) 7 8 +1
GET /related-baseline (cold) 4 5 +1
GET /related-baseline (warm) 4 5 +1
GET /rss.xml (cold) 1 2 +1
GET /rss.xml (warm) 1 2 +1
GET /tag/webdev (cold) 8 9 +1
GET /tag/webdev (warm) 8 9 +1

D1

Route Before After Δ
GET / (cold) 20 22 +2
GET / (warm) 4 5 +1
GET /category/development (cold) 20 21 +1
GET /category/development (warm) 8 9 +1
GET /contributors (cold) 14 15 +1
GET /contributors (warm) 4 5 +1
GET /contributors-naive (cold) 21 22 +1
GET /contributors-naive (warm) 11 12 +1
GET /posts (cold) 14 15 +1
GET /posts (warm) 4 5 +1
GET /posts/building-for-the-long-term (cold) 23 24 +1
GET /posts/building-for-the-long-term (warm) 12 13 +1
GET /related (cold) 17 18 +1
GET /related (warm) 7 8 +1
GET /related-baseline (cold) 14 15 +1
GET /related-baseline (warm) 4 5 +1
GET /rss.xml (cold) 10 11 +1
GET /rss.xml (warm) 1 2 +1
GET /tag/webdev (cold) 20 21 +1
GET /tag/webdev (warm) 8 9 +1

Comparing snapshot files between base and head. Updated automatically on each push.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://emdash-bot-issue-4675.try.emdashcms.com, https://emdash-bot-issue-4675-emdash-playground.emdash-cms.workers.dev (commit b3dc7a5)

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://beac42b7.try.emdashcms.com, https://beac42b7-emdash-playground.emdash-cms.workers.dev b3dc7a5 2026-11-08T00:48:22.052Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://aadabec1.try.emdashcms.com, https://aadabec1-emdash-playground.emdash-cms.workers.dev 538281d 2026-11-08T00:42:46.197Z 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.

The approach is sound. Cold-request COUNT(*) scans are replaced by bounded existence probes (a missing table, an empty _docsize, and a short-circuiting EXISTS for live content). Explicit verification keeps its previous default behavior. Both reviewers found no implementation bug in the changed paths. Both flagged that the manual-repair guidance for partial indexes is incomplete: it does not say that POST /_emdash/api/search/rebuild requires the Admin-only search:manage permission. One reviewer also wants the recovery procedure added to the canonical docs. One test can also pass without capturing any queries.


Reviewed by anthropic/claude-opus-5-5 and openai/gpt-6.1-sol.

Comment thread .changeset/cold-search-health.md Outdated
"emdash": patch
---

Reduces D1 rows read by search and autocomplete on cold Workers isolates by skipping full row-count verification of existing search indexes. Missing indexes and indexes left empty by an interrupted rebuild are still repaired automatically. Partially populated indexes are no longer detected automatically; to repair one, send an authenticated `POST /_emdash/api/search/rebuild` request with `X-EmDash-Request: 1` and a JSON body such as `{"collection":"articles"}`.

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.

[needs fixing] The recovery steps say to send "an authenticated POST /_emdash/api/search/rebuild request". That route requires search:manage, which packages/auth/src/rbac.ts maps to Admin only. An Editor, or a token without that permission, gets a 403. Name the prerequisite, for example: "...as an Admin (or with a token that has search:manage), with the X-EmDash-Request: 1 header and a JSON body such as {"collection":"articles"}."

The other reviewer adds that this paragraph is the only documentation of the change to partial-index recovery. The canonical search sections in docs/src/content/docs/reference/rest-api.mdx do not explain the missing/empty-only health check or how to recover partial indexes. .changeset/README.md expects useful explanations and examples in canonical feature or upgrade docs too. Add the recovery procedure there and link it from this entry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the changeset to require the Admin-only search:manage permission and, for Bearer tokens, the admin scope. Added a linked “Search index recovery” section to the REST API reference with the health-check limits, a complete rebuild request, and verification steps. The docs build passes.

Comment on lines +99 to +101
expect(
statements.filter((statement) => /CREATE|DROP|INSERT|count\s*\(/i.test(statement)),
).toEqual([]);

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] This test only checks that no CREATE/DROP/INSERT/COUNT statements were captured. If the spy captured nothing, the filter would return [] and the test would still pass. The cold-runtime test above guards against this with expect(statements.length).toBeGreaterThan(0). Add the same assertion here.

Suggested change
expect(
statements.filter((statement) => /CREATE|DROP|INSERT|count\s*\(/i.test(statement)),
).toEqual([]);
expect(statements.length).toBeGreaterThan(0);
expect(
statements.filter((statement) => /CREATE|DROP|INSERT|count\s*\(/i.test(statement)),
).toEqual([]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added the non-empty query-capture assertion. All 7 FTS repair tests pass.

@pkg-pr-new

pkg-pr-new Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-test

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-loader

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

@emdash-cms/registry-moderation

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

@emdash-cms/registry-verification

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

upgrade-emdash

npm i https://pkg.pr.new/upgrade-emdash@4760

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-ai-search

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-search@4760

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: b3dc7a5

Addressed review feedback: documented Admin-only search:manage permission and Bearer admin scope in the changeset and canonical REST API recovery procedure; added a non-empty query-capture assertion. Verified docs build, all 7 FTS repair tests, formatting, and quick lint with zero diagnostics. Independent review found no blockers.

EmDash-Bot-Case: 1198246700-issue-6226228056
EmDash-Bot-Candidate: cand-2-6f9fa078d4
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs b3dc7a5 Nov 08 2026, 12:47 AM

@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. Cold-isolate health checks now use bounded existence probes (missing table, empty _docsize, short-circuiting EXISTS for live content) instead of two full COUNT(*) scans per collection. Explicit verifyAndRepairAll() and verifyAndRepairIndex() calls keep their default row-count checks. The trade-off is that partially populated indexes are no longer repaired automatically, and the PR states this openly. Both reviewers approved, and neither found any remaining issues. The earlier findings are resolved: the changeset now names the Admin-only search:manage permission and the Bearer-token admin scope, a linked "Search index recovery" section was added to the REST API reference, and the empty-collection test now asserts that queries were captured.


Reviewed by anthropic/claude-opus-5-5 and openai/gpt-6.1-sol.

@emdash-autofix emdash-autofix Bot added the triage/awaiting-reporter Fix pushed, waiting on reporter to verify label Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core area/docs bot:fix Assign this issue or pull request to the EmDash bot to fix cla: signed query-count changed PR diff modifies query-count snapshot files size/M triage/awaiting-reporter Fix pushed, waiting on reporter to verify

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Search health check counts every row of each searchable collection on each new isolate (~114k rows × 348 a day on D1)

0 participants