Skip to content

Commit da4697a

Browse files
feat(results): cap SELECT result rows (default 500) with a 100/500/1k/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>
1 parent 2b5af6c commit da4697a

12 files changed

Lines changed: 257 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
1010
## [Unreleased]
1111

1212
### Added
13+
- **Result-row cap** with a 100 / 500 / 1000 / 5000 / 10000 selector in the result
14+
toolbar (default **500**, a global preference persisted across tabs and reloads).
15+
A normal `SELECT` now fetches at most the selected cap rather than pulling every
16+
row over the wire: ClickHouse stops cleanly at the cap server-side
17+
(`max_result_rows` + `result_overflow_mode = 'break'`), a small client-side guard
18+
trims the block-boundary overage `break` can leave, and a **"first N (capped)"**
19+
badge appears in the stats row when the limit is hit. Changing the selector
20+
re-runs the current query, so raising the cap genuinely fetches more. The display
21+
grid now renders up to the selected cap (10000 actually shows 10000). EXPLAIN /
22+
PIPELINE / ESTIMATE runs are exempt. (#86)
1323
- Playwright e2e now runs on **WebKit** in addition to Chromium and Firefox, so
1424
many Safari regressions on the `html{zoom}`-based layout fail CI instead of
1525
shipping silently. README gained a **Supported browsers** stance: desktop

src/core/stream.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@
77
// `applyStreamLine` folds one parsed object into a mutable result; keeping it
88
// pure (no fetch, no DOM) makes the streaming parser fully unit-testable.
99

10-
/** A fresh, empty result object for a query run in output format `fmt`. */
11-
export function newResult(fmt) {
10+
/**
11+
* A fresh, empty result object for a query run in output format `fmt`. `rowLimit`
12+
* (default 0 = uncapped) is the client-side row cap: the server's
13+
* result_overflow_mode='break' stops at the cap but can overshoot to the next
14+
* block boundary, so applyStreamLine trims any rows past `rowLimit` and flags
15+
* `capped` once it's reached.
16+
*/
17+
export function newResult(fmt, rowLimit = 0) {
1218
return {
1319
columns: [],
1420
rows: [],
@@ -18,6 +24,8 @@ export function newResult(fmt) {
1824
error: null,
1925
cancelled: false,
2026
pct: 0,
27+
rowLimit,
28+
capped: false,
2129
};
2230
}
2331

@@ -26,7 +34,12 @@ export function applyStreamLine(json, result) {
2634
if (json.meta) {
2735
result.columns = json.meta.map((m) => ({ name: m.name, type: m.type }));
2836
} else if (json.row) {
29-
result.rows.push(result.columns.map((c) => json.row[c.name]));
37+
// At the cap: drop the row (block-boundary overage from `break`) and flag it.
38+
if (result.rowLimit > 0 && result.rows.length >= result.rowLimit) {
39+
result.capped = true;
40+
} else {
41+
result.rows.push(result.columns.map((c) => json.row[c.name]));
42+
}
3043
} else if (json.progress) {
3144
const p = json.progress;
3245
const total = +p.total_rows_to_read || 0;

src/net/ch-client.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ export async function loadEntityDoc(ctx, name, sqlString) {
370370
*
371371
* @param ctx
372372
* @param sql
373-
* @param o { format, signal, onLine(json), onChunk(), onRaw(text) }
373+
* @param o { format, signal, resultRowLimit, onLine(json), onChunk(), onRaw(text) }
374374
*/
375375
export async function runQuery(ctx, sql, o = {}) {
376376
const fmt = o.format || 'Table';
@@ -383,14 +383,24 @@ export async function runQuery(ctx, sql, o = {}) {
383383
: fmt === 'TSV'
384384
? 'TabSeparatedWithNamesAndTypes'
385385
: fmt;
386+
// Cap a normal result query server-side: max_result_rows stops the read at N
387+
// and result_overflow_mode='break' makes ClickHouse stop cleanly at a block
388+
// boundary (no error, no further data pulled) rather than throwing. The caller
389+
// decides scope — it passes resultRowLimit for normal SELECTs (Table + explicit
390+
// FORMAT) and 0 for EXPLAIN/PIPELINE/ESTIMATE (which also run as 'Table', so the
391+
// exemption can't be told apart by format here). `break` can overshoot by up to
392+
// a block on the streaming path, which the applyStreamLine guard trims.
393+
const cap = o.resultRowLimit > 0
394+
? { max_result_rows: o.resultRowLimit, result_overflow_mode: 'break' }
395+
: {};
386396
const url = chUrl(ctx.origin, {
387397
format: fmtParam,
388398
// wait_end_of_query buffers the whole response server-side so the HTTP
389399
// status reflects errors — but it defeats progressive streaming (first rows
390400
// wait for the query to finish: ~16s vs ~0.5s on a 1.3M-row scan). Keep it
391401
// only for raw modes (read whole anyway); the streaming Table path drops it
392402
// and surfaces mid-stream errors via the in-band `exception` line instead.
393-
extra: { ...(isStreaming ? {} : { wait_end_of_query: 1 }), add_http_cors_header: 1 },
403+
extra: { ...(isStreaming ? {} : { wait_end_of_query: 1 }), ...cap, add_http_cors_header: 1 },
394404
// Tagging the request with a query_id lets Cancel issue KILL QUERY for it.
395405
params: o.queryId ? { query_id: o.queryId } : {},
396406
});

src/state.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,20 @@ export const KEYS = {
2525
saved: 'asb:saved',
2626
history: 'asb:history',
2727
libraryName: 'asb:libraryName',
28+
resultRowLimit: 'asb:resultRowLimit',
2829
};
2930

31+
/** Row-limit options for the result cap selector (shared between state + UI). */
32+
export const RESULT_ROW_LIMIT_OPTIONS = [100, 500, 1000, 5000, 10000];
33+
34+
/** Default row cap when none is persisted (or a stored value is unrecognized). */
35+
export const DEFAULT_RESULT_ROW_LIMIT = 500;
36+
37+
/** Snap a row-limit to a known option, falling back to the default. Pure. */
38+
export function normalizeRowLimit(n) {
39+
return RESULT_ROW_LIMIT_OPTIONS.includes(n) ? n : DEFAULT_RESULT_ROW_LIMIT;
40+
}
41+
3042
/** Default name for a fresh / unnamed saved-query library. */
3143
export const DEFAULT_LIBRARY_NAME = 'SQL Library';
3244

@@ -46,6 +58,11 @@ export function createState(read = { loadJSON, loadStr }) {
4658
nextTabId: 2,
4759
theme: read.loadStr(KEYS.theme, 'light'),
4860
density: 'comfortable',
61+
// Global cap on how many rows a normal SELECT fetches (server-side
62+
// max_result_rows + a client-side guard; see runQuery / applyStreamLine).
63+
// One persisted preference, default 500; a non-option stored value snaps
64+
// back to the default so the selector always reflects a real choice.
65+
resultRowLimit: normalizeRowLimit(parseInt(read.loadStr(KEYS.resultRowLimit, '500'), 10)),
4966
sidebarPx: clamp(parseInt(read.loadStr(KEYS.sidebarPx, '248'), 10), 180, 420),
5067
editorPct: num(KEYS.editorPct, 45, 15, 85),
5168
sideSplitPct: num(KEYS.sideSplitPct, 58, 25, 85),

src/styles.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,6 +1470,26 @@ table.res-table tbody tr:hover td.idx { background: var(--bg-hover); }
14701470
padding: 2px 7px; border-radius: 4px;
14711471
background: color-mix(in oklab, #ef4444 12%, transparent);
14721472
}
1473+
.capped-badge {
1474+
font-family: var(--mono); font-size: 10.5px; color: var(--accent);
1475+
padding: 2px 7px; border-radius: 4px;
1476+
background: color-mix(in oklab, var(--accent) 12%, transparent);
1477+
white-space: nowrap;
1478+
}
1479+
1480+
/* Result row-cap selector (result toolbar, after the view tabs). */
1481+
.row-limit { display: flex; align-items: center; gap: 6px; }
1482+
.row-limit-label {
1483+
font-size: 10.5px; color: var(--fg-faint);
1484+
text-transform: uppercase; letter-spacing: .05em;
1485+
}
1486+
.row-limit-select {
1487+
height: 24px;
1488+
background: var(--bg-input); border: 1px solid var(--border);
1489+
border-radius: 5px; color: var(--fg);
1490+
font-family: inherit; font-size: 11.5px;
1491+
padding: 0 6px; outline: none; cursor: pointer;
1492+
}
14731493

14741494
/* scrollbars */
14751495
*::-webkit-scrollbar { width: 10px; height: 10px; }

src/ui/app.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { h, zoomScale, fixedAnchor } from './dom.js';
88
import { Icon } from './icons.js';
99
import {
10-
createState, activeTab, KEYS, recordHistory, saveQuery, savedForTab, tabChart,
10+
createState, activeTab, KEYS, recordHistory, saveQuery, savedForTab, tabChart, normalizeRowLimit,
1111
} from '../state.js';
1212
import { saveJSON, saveStr } from '../core/storage.js';
1313
import { decodeJwtPayload, isTokenExpired } from '../core/jwt.js';
@@ -471,8 +471,13 @@ export function createApp(env = {}) {
471471
fmt = explicitFmt || 'Table';
472472
}
473473

474+
// Cap a normal result query (Table or explicit-FORMAT SELECT) at the global
475+
// row limit; EXPLAIN/PIPELINE/ESTIMATE are exempt (small output, and a cap
476+
// would truncate a plan oddly). The streaming guard reads it off the result;
477+
// runQuery adds the server-side max_result_rows for the Table path.
478+
const rowLimit = explainMode ? 0 : app.state.resultRowLimit;
474479
const t0 = now();
475-
tab.result = newResult(fmt);
480+
tab.result = newResult(fmt, rowLimit);
476481
if (explainView) tab.result.explainView = explainView;
477482
app.state.resultSort = { col: null, dir: 'asc' };
478483
app.state.runT0 = t0;
@@ -494,6 +499,7 @@ export function createApp(env = {}) {
494499
try {
495500
const out = await ch.runQuery(chCtx, runSql, {
496501
format: fmt,
502+
resultRowLimit: rowLimit,
497503
queryId: app.state.runQueryId,
498504
signal: app.state.abortController.signal,
499505
onLine: (json) => applyStreamLine(json, tab.result),
@@ -652,6 +658,15 @@ export function createApp(env = {}) {
652658
function explainQuery() { return run({ explain: true }); }
653659
// Switch the active EXPLAIN view (re-runs the derived query, keeps the mode).
654660
function setExplainView(id) { return run({ explainView: id }); }
661+
// Change the global result-row cap: persist the (normalized) preference and
662+
// re-run the current query so a raise genuinely fetches more (server-side cap),
663+
// a lower one stops sooner. run() no-ops on an empty editor, so changing the
664+
// limit with nothing typed just saves the preference.
665+
function setResultRowLimit(n) {
666+
app.state.resultRowLimit = normalizeRowLimit(n);
667+
app.savePref('resultRowLimit', app.state.resultRowLimit);
668+
return run();
669+
}
655670

656671
// Fetch the DDL for `target` (e.g. 'db.table' or 'DATABASE db') with
657672
// SHOW CREATE, pretty-print it through formatQuery(), and drop it into the
@@ -863,6 +878,7 @@ export function createApp(env = {}) {
863878
formatQuery,
864879
explainQuery,
865880
setExplainView,
881+
setResultRowLimit,
866882
showSchemaGraph,
867883
expandSchemaGraph,
868884
openNodeDetail,

src/ui/results.js

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { looksLikeHtml, prettyValue } from '../core/cell.js';
99
import { sortRows } from '../core/sort.js';
1010
import { autoChart, schemaKey, chartFieldOptions, chartColors, chartJsConfig, chartCfgValid, normalizeChartCfg, unzoomChartEvent, CHART_ROW_CAP } from '../core/chart-data.js';
1111
import { EXPLAIN_VIEWS } from '../core/explain.js';
12+
import { RESULT_ROW_LIMIT_OPTIONS } from '../state.js';
1213
import { renderExplainGraph, openPipelineFullscreen, renderSchemaGraph } from './explain-graph.js';
1314

1415
// View id → tab glyph for the EXPLAIN view strip (kept here so core/explain.js
@@ -18,9 +19,19 @@ const EXPLAIN_ICONS = {
1819
pipeline: Icon.share, estimate: Icon.rows,
1920
};
2021

21-
const VIS_CAP = 5000;
22+
const VIS_CAP = 5000; // fallback display cap for results that carry no row limit (raw / EXPLAIN)
2223
const MIN_COL = 48; // px floor for a resized column
2324

25+
/**
26+
* How many rows to render: follow the result's own row cap when set (so a 10000
27+
* limit renders 10000), else the fixed fallback. The server cap already trims a
28+
* normal SELECT to its limit, so this just keeps the renderers from re-capping
29+
* a large-but-allowed result. Pure — exported for tests.
30+
*/
31+
export function visCap(r) {
32+
return r.rowLimit > 0 ? r.rowLimit : VIS_CAP;
33+
}
34+
2435
/**
2536
* New width (px) for a column dragged by `dx` client px. `scale` converts client
2637
* px → CSS px under the page `zoom` (computed per element); 0/NaN falls back to
@@ -149,6 +160,27 @@ function streamStrip(r) {
149160
: h('i', { class: 'sweep' }));
150161
}
151162

163+
/**
164+
* A <select> capping how many rows a normal query fetches (the global, persisted
165+
* preference). Changing it re-runs the current query with the new server-side
166+
* cap, so a higher limit genuinely fetches more. The caller hides it for EXPLAIN
167+
* views (small output a cap would truncate oddly).
168+
*/
169+
function rowLimitSelect(app) {
170+
const sel = h('select', {
171+
class: 'row-limit-select',
172+
title: 'Max rows to fetch — changing re-runs the query',
173+
onchange: (e) => app.actions.setResultRowLimit(Number(e.target.value)),
174+
});
175+
for (const n of RESULT_ROW_LIMIT_OPTIONS) {
176+
sel.appendChild(h('option', { value: String(n) }, String(n)));
177+
}
178+
// Reflect the current limit by value (set after the options are attached so the
179+
// <select> resolves the selection correctly).
180+
sel.value = String(app.state.resultRowLimit);
181+
return h('label', { class: 'row-limit' }, h('span', { class: 'row-limit-label' }, 'Rows'), sel);
182+
}
183+
152184
function buildToolbar(app, r) {
153185
const toolbar = h('div', { class: 'res-toolbar' });
154186
if (r && r.schemaGraph) {
@@ -196,6 +228,9 @@ function buildToolbar(app, r) {
196228
}
197229
}
198230
toolbar.appendChild(tabs);
231+
// Row-cap selector after the view tabs, for normal result queries only —
232+
// EXPLAIN views are exempt (small output a cap would truncate oddly).
233+
if (!(r && r.explainView)) toolbar.appendChild(rowLimitSelect(app));
199234
toolbar.appendChild(h('div', { style: { flex: '1' } }));
200235
// EXPLAIN views suppress the ms/rows/bytes stats — they're not meaningful for a
201236
// plan and the freed space lets the five tabs breathe.
@@ -226,6 +261,13 @@ function buildToolbar(app, r) {
226261
h('span', { class: 'v' }, (r.rawText != null ? '—' : r.rows.length) + ' rows')));
227262
toolbar.appendChild(h('div', { class: 'stat', title: r.progress.rows + ' rows scanned' },
228263
h('span', { class: 'ic' }, Icon.bytes()), h('span', { class: 'v' }, formatBytes(r.progress.bytes))));
264+
// The result hit the row cap: say so (the fetch stopped at the limit, more
265+
// rows exist). Only the streaming path sets `capped`; raw output can't.
266+
if (r.capped) {
267+
toolbar.appendChild(h('span', {
268+
class: 'capped-badge', title: 'Fetch stopped at the row limit — raise it to see more',
269+
}, 'first ' + r.rowLimit + ' (capped)'));
270+
}
229271
}
230272
if (r.explainView === 'pipeline' && r.rawText && !r.error) {
231273
toolbar.appendChild(h('button', {
@@ -248,7 +290,7 @@ function buildToolbar(app, r) {
248290
}
249291

250292
export function renderJson(r) {
251-
const arr = r.rows.slice(0, VIS_CAP).map((row) => {
293+
const arr = r.rows.slice(0, visCap(r)).map((row) => {
252294
const o = {};
253295
r.columns.forEach((c, i) => { o[c.name] = row[i]; });
254296
return o;
@@ -294,7 +336,8 @@ export function renderTable(app, r) {
294336
if (Object.keys(r.colWidths).length) applyFixedWidths(table, r);
295337

296338
const tbody = document.createElement('tbody');
297-
rows.slice(0, VIS_CAP).forEach((row, ri) => {
339+
const cap = visCap(r);
340+
rows.slice(0, cap).forEach((row, ri) => {
298341
const tr = document.createElement('tr');
299342
tr.appendChild(h('td', { class: 'idx' }, String(ri + 1)));
300343
row.forEach((v, ci) => {
@@ -312,10 +355,10 @@ export function renderTable(app, r) {
312355
});
313356
table.appendChild(tbody);
314357
wrap.appendChild(table);
315-
if (rows.length > VIS_CAP) {
358+
if (rows.length > cap) {
316359
wrap.appendChild(h('div', {
317360
style: { padding: '10px 14px', fontSize: '11px', color: 'var(--fg-faint)', fontFamily: 'var(--mono)', borderTop: '1px solid var(--border)' },
318-
}, '… + ' + (rows.length - VIS_CAP) + ' more rows truncated for display.'));
361+
}, '… + ' + (rows.length - cap) + ' more rows truncated for display.'));
319362
}
320363
return wrap;
321364
}

tests/helpers/fake-app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export function makeApp(over = {}) {
7373
formatQuery: vi.fn(),
7474
explainQuery: vi.fn(),
7575
setExplainView: vi.fn(),
76+
setResultRowLimit: vi.fn(),
7677
showSchemaGraph: vi.fn(),
7778
expandSchemaGraph: vi.fn(),
7879
openNodeDetail: vi.fn(),

tests/unit/app.test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,45 @@ describe('query run', () => {
475475
await app.actions.run();
476476
expect(app.activeTab().result.rawFormat).toBe('JSON'); // FORMAT clause, not the EXPLAIN default
477477
});
478+
const runUrl = (e, re) => e.fetch.mock.calls.findLast((c) => re.test((c[1] && c[1].body) || ''))[0];
479+
it('caps a normal SELECT server-side and trims block-boundary overage (flagging capped)', async () => {
480+
const { app, e } = appForRun([
481+
[(u, sql) => /SELECT 1/.test(sql), resp({ body: streamBody([
482+
'{"meta":[{"name":"a","type":"UInt8"}]}\n',
483+
'{"row":{"a":"1"}}\n', '{"row":{"a":"2"}}\n', '{"row":{"a":"3"}}\n', // overage past the cap of 2
484+
]) })],
485+
]);
486+
app.state.resultRowLimit = 2;
487+
app.activeTab().sql = 'SELECT 1';
488+
await app.actions.run();
489+
const url = runUrl(e, /SELECT 1/);
490+
expect(url).toContain('max_result_rows=2');
491+
expect(url).toContain('result_overflow_mode=break');
492+
expect(app.activeTab().result.rows).toEqual([['1'], ['2']]); // overage trimmed client-side
493+
expect(app.activeTab().result.capped).toBe(true);
494+
});
495+
it('does not cap EXPLAIN/ESTIMATE runs even though ESTIMATE streams as Table', async () => {
496+
const { app, e } = appForRun([
497+
[(u, sql) => /ESTIMATE/.test(sql), resp({ body: streamBody(['{"meta":[{"name":"rows","type":"UInt64"}]}\n', '{"row":{"rows":"42"}}\n']) })],
498+
]);
499+
app.state.resultRowLimit = 100;
500+
app.activeTab().sql = 'EXPLAIN ESTIMATE SELECT 1';
501+
await app.actions.run();
502+
expect(runUrl(e, /ESTIMATE/)).not.toContain('max_result_rows');
503+
expect(app.activeTab().result.capped).toBe(false);
504+
});
505+
it('setResultRowLimit persists the normalized preference and re-runs with the new cap', async () => {
506+
const { app, e } = appForRun([
507+
[(u, sql) => /SELECT 1/.test(sql), resp({ body: streamBody(['{"meta":[{"name":"a","type":"UInt8"}]}\n', '{"row":{"a":"1"}}\n']) })],
508+
]);
509+
app.activeTab().sql = 'SELECT 1';
510+
await app.actions.setResultRowLimit(99); // not an option → snaps back to the default 500
511+
expect(app.state.resultRowLimit).toBe(500);
512+
expect(globalThis.localStorage.getItem('asb:resultRowLimit')).toBe('500');
513+
await app.actions.setResultRowLimit(1000);
514+
expect(app.state.resultRowLimit).toBe(1000);
515+
expect(runUrl(e, /SELECT 1/)).toContain('max_result_rows=1000'); // re-ran with the new cap
516+
});
478517
});
479518

480519
describe('formatQuery', () => {

tests/unit/ch-client.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,16 @@ describe('runQuery', () => {
306306
await runQuery(raw, 'x', { format: 'TSV' });
307307
expect(raw.fetch.mock.calls[0][0]).toContain('wait_end_of_query=1');
308308
});
309+
it('adds the server-side row cap when resultRowLimit is set; omits it otherwise', async () => {
310+
const capped = ctxWith(async () => streamResp(['{"row":{}}\n']));
311+
await runQuery(capped, 'x', { format: 'Table', resultRowLimit: 500 });
312+
const url = capped.fetch.mock.calls[0][0];
313+
expect(url).toContain('max_result_rows=500');
314+
expect(url).toContain('result_overflow_mode=break');
315+
const uncapped = ctxWith(async () => streamResp(['{"row":{}}\n']));
316+
await runQuery(uncapped, 'x', { format: 'Table' }); // no limit → no cap params
317+
expect(uncapped.fetch.mock.calls[0][0]).not.toContain('max_result_rows');
318+
});
309319
});
310320

311321
describe('killQuery', () => {

0 commit comments

Comments
 (0)