Skip to content

feat(query,viewer): add viewer_external_links read model for /api/links?type=external#157

Merged
YusukeHirao merged 3 commits into
feature/impl-new-dbfrom
feature/external-links-read-model
Jul 3, 2026
Merged

feat(query,viewer): add viewer_external_links read model for /api/links?type=external#157
YusukeHirao merged 3 commits into
feature/impl-new-dbfrom
feature/external-links-read-model

Conversation

@YusukeHirao

Copy link
Copy Markdown
Member

Summary

  • listExternalLinks ran a live anchors JOIN + GROUP BY + COUNT(DISTINCT ...) per request, twice (once for the total, once for the page of data) — SQLite is known to build a separate b-tree for COUNT(DISTINCT) instead of reusing an index, and this query had no measurement or caching unlike every other heavy read in this package.
  • Add viewer_external_links, populated once per buildViewerReadModel run by computeExternalLinkRows (aggregation logic lifted verbatim from listExternalLinks, so referrerCount stays in lockstep with getPageDetail.inboundLinks, fix(crawler): http と https の同一 URL が別ページとして二重登録される #71).
  • listViewerExternalLinks serves reads from the pre-aggregated table via plain paginateQuery — no keyset cursor needed since the REST contract is already offset-based.
  • register-links-route.ts now dispatches to the fast path when the read model is current, falling back to the untouched listExternalLinks otherwise — the same two-layer pattern register-pages-route.ts already uses for /api/pages.
  • Bumps VIEWER_READ_MODEL_SCHEMA_VERSION 4→5; existing archives rebuild automatically on next open.
  • Backend-only change — REST response shape and frontend are unchanged.

Test plan

  • yarn build (+ direct tsc per-package check to rule out sandboxed no-op builds)
  • yarn test (2385 tests, full suite)
  • yarn lint (0 errors)
  • New tests: compute-external-link-rows.spec.ts, list-viewer-external-links.spec.ts (mirrors list-external-links.spec.ts's full case set), build-viewer-read-model.spec.ts additions, register-links-route.spec.ts (fast-path/legacy-fallback dual fixture)
  • xhigh code-review pass (0 findings) + qa-engineer pass (1 stale JSDoc count fixed) + product-manager pass (1 missing @example fixed) + doc pass (lint clean)

🤖 Generated with Claude Code

…ink listing

listExternalLinks ran a live anchors JOIN + GROUP BY + COUNT(DISTINCT ...)
per request, twice (once for the total, once for the page of data).
SQLite is known to build a separate b-tree for COUNT(DISTINCT) instead of
reusing an index, so this scaled poorly and had no measurement or caching
unlike every other heavy read in this package.

Add viewer_external_links, populated once per buildViewerReadModel run by
computeExternalLinkRows (the aggregation logic lifted verbatim from
listExternalLinks so referrer-counting semantics stay in lockstep with
getPageDetail.inboundLinks). listViewerExternalLinks serves reads from the
pre-aggregated table via plain paginateQuery, with no keyset cursor needed
since the REST contract is already offset-based. listExternalLinks itself
is untouched and remains the fallback for archives without a current read
model. Bump VIEWER_READ_MODEL_SCHEMA_VERSION 4->5 for the new table.
…fast path

/api/links?type=external now dispatches to listViewerExternalLinks when
the viewer_external_links read model is current, falling back to the
legacy listExternalLinks otherwise -- the same two-layer pattern
register-pages-route.ts already uses for /api/pages. No unsupported
filter forces a legacy fallback here, since urlPattern/status both map
directly onto viewer_external_links columns.
…onale

Explain why listExternalLinks's live GROUP BY + COUNT(DISTINCT ...) query
was moved into the viewer_external_links read model: SQLite is known to
build a separate b-tree for COUNT(DISTINCT) instead of reusing an index,
and expression-based indexes only help when the indexed expression
matches the query verbatim -- neither escape hatch was reliable here, so
materialising the aggregate once (the pattern this package already uses
for viewer_pages/viewer_directory_nodes) was the safer fix.
@YusukeHirao YusukeHirao merged commit d5b54f4 into feature/impl-new-db Jul 3, 2026
4 checks passed
@YusukeHirao YusukeHirao deleted the feature/external-links-read-model branch July 3, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant