Skip to content

feat(results): cap SELECT result rows (default 500) with a 100/500/1k/5k/10k selector#94

Merged
BorisTyshkevich merged 1 commit into
mainfrom
feat/result-row-cap-86
Jun 30, 2026
Merged

feat(results): cap SELECT result rows (default 500) with a 100/500/1k/5k/10k selector#94
BorisTyshkevich merged 1 commit into
mainfrom
feat/result-row-cap-86

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What & why

Closes #86 (Phase 2 of the roadmap #68).

A SELECT over a huge table used to pull every row over the wire (only the display was capped at 5000) — wasting bandwidth/memory and able to hang the tab. This adds a real fetch cap.

Hybrid mechanism (the settled design in #86):

  • Server-sidemax_result_rows = N + result_overflow_mode = 'break' so ClickHouse stops cleanly at the cap (no error, no full pull; it stops after the block that crosses N).
  • Client-side guardapplyStreamLine trims the block-boundary overage break can leave and flags result.capped.

UX

  • A 100 / 500 / 1000 / 5000 / 10000 selector in the result toolbar (after the view tabs; hidden for EXPLAIN views). Default 500.
  • Global, persisted preference (one localStorage key, like theme/splitters) — applies to all tabs, survives reload.
  • Changing it re-runs the current query with the new server-side cap, so raising it genuinely fetches more.
  • A "first N (capped)" badge in the stats row when the cap is hit.
  • The display grid now renders up to the selected cap (10000 actually shows 10000, not the old fixed 5000).
  • EXPLAIN / PIPELINE / ESTIMATE are exempt (small output; a cap would truncate a plan oddly). The scope decision lives in app.js (it knows explainMode); ch-client just honors the caller's limit, since ESTIMATE also runs as Table and can't be told apart by format.

Files

  • src/state.jsKEYS.resultRowLimit, resultRowLimit (default 500, from localStorage), RESULT_ROW_LIMIT_OPTIONS, pure normalizeRowLimit.
  • src/net/ch-client.jsrunQuery honors o.resultRowLimit via the existing extra dict.
  • src/core/stream.jsnewResult(fmt, rowLimit) carries the cap + capped; applyStreamLine stops at the cap. Pure, 100%.
  • src/ui/results.js — selector, capped badge, visCap() display cap.
  • src/ui/app.jssetResultRowLimit action; run path passes the cap for normal SELECTs only.

Checklist

  • npm test passes (per-file 100/100/100/100 gate holds)
  • Tests added/updated in the same change (state / stream / net / results / app)
  • npm run build succeeds (single-file dist/sql.html, no new runtime dep)
  • Layers kept honest: pure logic in src/core/, network in src/net/ (injected fetch), DOM in src/ui/
  • No new runtime dependency
  • CHANGELOG.md [Unreleased] updated (deployed HTTP surface unchanged — only query-string params added; no http_handlers.xml/README change)
  • Reconciled tracked work: CHANGELOG done; ADR-0001 unaffected (resultRowLimit is a plain field like theme/density, not a signal); roadmap Roadmap to 1.0.0 #68 Phase-2 box to tick on merge

Verification

  • Unit + integration: app.test.js exercises the real run() path — asserts max_result_rows/result_overflow_mode=break reach the URL for a normal SELECT, the client guard trims overage, capped is set, and EXPLAIN/ESTIMATE send no cap. results.test.js covers the selector (renders/reflects/re-runs), the badge, and visCap.
  • e2e: npm run test:e2e green on Chromium / Firefox / WebKit (39 passed).
  • A live browser verify (real ClickHouse + OAuth) wasn't run — no backend available in this environment; behavior is covered by the integration test above.

🤖 Generated with Claude Code

…/5k/10k selector

A normal SELECT no longer pulls every row over the wire — it fetches at most a
selected cap (default 500). Hybrid mechanism per #86: ClickHouse stops cleanly
server-side (`max_result_rows` + `result_overflow_mode='break'`), and a small
client-side guard in `applyStreamLine` trims the block-boundary overage `break`
can leave, flagging `result.capped`.

- `src/state.js` — `KEYS.resultRowLimit` + `resultRowLimit` (default 500, read
  from localStorage), `RESULT_ROW_LIMIT_OPTIONS`, and a pure `normalizeRowLimit`
  that snaps a stored/selected value back to a known option.
- `src/net/ch-client.js` — `runQuery` honors `o.resultRowLimit`, adding the cap
  params via the existing `extra` dict. Scope is decided by the caller (app.js
  passes 0 for EXPLAIN/PIPELINE/ESTIMATE, which also run as `Table` and so can't
  be told apart by format here).
- `src/core/stream.js` — `newResult(fmt, rowLimit=0)` carries the cap + `capped`;
  `applyStreamLine` stops pushing past the cap and flags `capped`. Pure, 100%.
- `src/ui/results.js` — a row-limit `<select>` after the view tabs (hidden for
  EXPLAIN), a "first N (capped)" badge in the stats row, and the display cap now
  follows the limit (`visCap`) so 10000 renders 10000 instead of the old 5000.
- `src/ui/app.js` — `setResultRowLimit` (persist pref + re-run so a raise fetches
  more), and the run path passes the cap for normal SELECTs only.

Tests added in the same change (state/stream/net/results) — per-file 100% gate
holds; build clean; e2e green on Chromium/Firefox/WebKit. Reconciles CHANGELOG
[Unreleased]. ADR-0001 unaffected (`resultRowLimit` is a plain field like
theme/density, not a signal).

Closes #86. Part of #68 (Phase 2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BorisTyshkevich BorisTyshkevich merged commit 06d3977 into main Jun 30, 2026
6 checks passed
BorisTyshkevich added a commit that referenced this pull request Jun 30, 2026
Reconcile the multiquery feature with #94 (result-row-cap selector), which
landed on main and overlapped the same surfaces:
- ch-client runQuery now carries BOTH o.resultRowLimit (single-query cap) and
  o.params (multiquery cap + session_id).
- results.js: kept the shared renderGrid extraction; it now takes a `cap` param
  so the main table honors the selectable limit (visCap) while the script-row
  pane uses the default. buildToolbar keeps the script branch (early return) and
  #94's row-limit selector + "capped" badge on the normal path.
- app.js: run() passes resultRowLimit (EXPLAIN-exempt) and sessionParamsFor;
  kept setResultRowLimit alongside the multiquery explain guard.
- CHANGELOG: both Added entries; tests: both suites kept.

1122 tests green; build OK; e2e green (39).
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.

Cap SELECT result rows (default 500) with a 100/500/1000/5000/10000 selector

1 participant