Skip to content

fix: skip byline query path when the bylines table is empty - #2304

Open
edrpls wants to merge 3 commits into
emdash-cms:mainfrom
edrpls:fix/byline-hydration-empty-table
Open

fix: skip byline query path when the bylines table is empty#2304
edrpls wants to merge 3 commits into
emdash-cms:mainfrom
edrpls:fix/byline-hydration-empty-table

Conversation

@edrpls

@edrpls edrpls commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes sites that never use bylines paying dead byline lookups on every content read.

hydrateEntryBylines falls back from the folded fast path to the full query path when any entry in the batch has an author_id but no folded credits (a batch-wide some()). That conservatism exists for a real reason — a credit in a different locale is invisible to the locale-correlated fold — but on a site whose _emdash_bylines table is empty, every authored entry matches that shape, so the fast path is effectively always dead. Every logged-out render pays getBylinesForEntries: a chunked _emdash_content_bylines lookup plus a _emdash_bylines-by-user_id lookup per locale bucket, all of which can only return zero rows.

Measured on a production deployment (Macabro festival site, emdash 0.31.1, 2,143 entries): these dead lookups were 25–45% of the emdash queries on every page render.

The fix folds an uncorrelated existence probe(SELECT 1 FROM _emdash_bylines LIMIT 1) — into the content query alongside the existing folded byline subquery. Uncorrelated scalar subqueries are evaluated once per statement on both SQLite and Postgres, so this adds zero round trips and no per-row cost (per the "logged-out hot path only ratchets down" rule; no caching or invalidation is involved because the signal rides the query it protects). An empty table makes an empty fold authoritative: no credit can exist in any locale, and the author fallback has no byline to resolve to. When the probe is missing (e.g. cached snapshots) or the table has rows, behavior is unchanged — including the deliberate locale-mismatch fallback.

Two tests drive the real loader (mocked astro:content delegating to emdashLoader()) on a real SQLite database with a query-counting log hook:

  • empty bylines table + authored entry → hydrated bylines: [] / byline: null with zero byline-table queries (fails on main with the two dead lookups);
  • byline linked to the author's user → the author fallback still resolves through the query path (guards against over-short-circuiting).

Found during a measured database audit of a production deployment. Note for CI: the query-count snapshot diff for logged-out routes should only go down or stay flat with this change.

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)
  • 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). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main. — n/a: no admin UI strings changed
  • 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 Fable 5 (Claude Code)

Screenshots / test output

Failing first (on main, before the fix) — the two dead lookups issued against an empty bylines table:

× skips the byline query path for authored entries when no bylines exist
  AssertionError: expected [ …(2) ] to deeply equal []
  + "select … from \"_emdash_content_bylines\" as \"cb\" inner join \"_emdash_bylines\" … where … \"content_id\" in (?) …"
  + "select … from \"_emdash_bylines\" as \"b\" … where \"b\".\"user_id\" in (?) …"

After the fix, full packages/core suite:

Test Files  1 failed | 392 passed | 1 skipped (394)
     Tests  1 failed | 5070 passed | 3 skipped (5074)

The single failure (virtual-modules.test.ts › watches resolved sandbox plugin entries) is pre-existing on a clean main checkout in this environment (macOS /var vs /private/var temp-dir realpath mismatch) and unrelated to this change.

🤖 Generated with Claude Code

The folded-byline fast path fell back to the full query path whenever any
entry in the batch had an author_id but no folded credits. On sites that
never use bylines every entry matches that shape, so every content read
paid getBylinesForEntries round trips (chunked content-byline lookups
plus a user-id lookup per locale bucket) that could only return zero rows.

Fold an uncorrelated existence probe on _emdash_bylines into the content
query (evaluated once per statement — no extra round trip). An empty
table makes an empty fold authoritative: no credit can exist in any
locale and the author fallback has no byline to resolve to, so hydration
serves the folded result directly. When the probe is missing (cached
snapshots) or the table has rows, the conservative fallback is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Jul 31, 2026
@changeset-bot

changeset-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 63c1a4e

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 Jul 31, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 63c1a4e

@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 addresses a real, measured performance problem: sites that never create bylines were paying getBylinesForEntries lookups on every content read because the fast-path fold was conservatively discarded for any authored entry. Folding an existence probe into the content SELECT so the empty-bylines case can trust the empty fold is a clever, stateless fix that avoids caching/invalidation complexity, and the tests drive the real SQLite loader with query-count assertions covering both the short-circuit and the non-short-circuit case.

I read the diff, full loader.ts/query.ts, the new test, and AGENTS.md. The logic is correct: the marker is set only when the folded column is present, missing markers are treated as "unknown" (safe fallback), and the author-fallback still falls through to the query path when bylines exist. The new folded column is also correctly excluded from data via SYSTEM_COLUMNS.

The one convention issue is that the code adds a SELECT 1 FROM _emdash_bylines LIMIT 1 existence probe. AGENTS.md is explicit: "Don't add a SELECT id FROM foo LIMIT 1 to skip work on empty sites — on live sites you pay the extra query every request for no gain." That rule does not currently carve out probes folded into the same statement. The author makes a reasonable case that this is zero extra round trips and was measured to help production deployments, but it is still the pattern the guideline prohibits. This needs either an AGENTS.md amendment or an alternative approach; until that is resolved I can't call it clean.

// 1 when `_emdash_bylines` has any row, NULL when empty. An empty table
// means an empty fold is authoritative — no credit in any locale, no
// author-fallback byline — so hydration can skip the byline query path.
const bylinesExist = sql`(SELECT 1 FROM ${sql.ref("_emdash_bylines")} LIMIT 1) AS ${sql.ref("_emdash_bylines_exist")}`;

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] Adds (SELECT 1 FROM _emdash_bylines LIMIT 1) as a folded existence probe. AGENTS.md states: "Don't add a SELECT id FROM foo LIMIT 1 to skip work on empty sites — on live sites you pay the extra query every request for no gain." This pattern matches that rule verbatim. While the PR reasonably folds it into the existing SELECT to avoid an extra round trip, the guideline as written prohibits any such probe. Please either update AGENTS.md to explicitly permit same-statement existence probes when they reduce total queries, or find a different mechanism (for example, a cheap per-epoch/bylines count cached at the loader level) that does not add a LIMIT 1 probe to every logged-out content fetch.

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.

Fair reading of the rule as written. I've amended AGENTS.md in this PR to carve out the same-statement shape explicitly: the rule's cost rationale (an extra round trip on every live request) doesn't apply to an uncorrelated scalar subquery folded into a select list the request already runs — SQLite and Postgres both evaluate it once per statement. The CI query-count snapshots on this PR confirm the shape: the recorded query texts changed, the per-route counts did not.

The alternative (a per-epoch cached count) was considered and rejected: it pays a real per-request options read on the query path and needs cross-isolate invalidation wiring on byline creation, which is strictly worse than a zero-round-trip probe with no invalidation at all. Happy to adjust the AGENTS.md wording if maintainers prefer different phrasing.

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Jul 31, 2026
The rule exists because a separate LIMIT 1 round trip costs every live
request. A probe folded into a query the request already runs has no
such cost, and the byline hydration fast path relies on exactly that
shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@edrpls
edrpls force-pushed the fix/byline-hydration-empty-table branch from d99abe7 to 63c1a4e Compare July 31, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core cla: signed review/needs-rereview Author pushed changes since the last review size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant