feat: multiquery + run-selection — run a ;-separated script (#83)#95
Merged
Conversation
…#83) The pure foundation for running a ;-separated script (DDL/INSERT/SELECT): - sql-split.js: lexical statement splitter that skips ; inside '…'/"…"/`…` literals (\' and '' escapes) and -- / # / block comments, plus an isRowReturning() classifier. - script-result.js: parse a JSONCompact SELECT body → {columns,rows,truncated} (the renderTable shape) and a comma-joined first-row preview. Both 100% covered. No wiring yet.
Additive o.params passthrough merged alongside query_id, so multiquery SELECTs can cap results server-side (max_result_rows / result_overflow_mode) without a new function or SQL mangling. Isolated so it can be reverted independently.
Run a ;-separated script (DDL / INSERT / SELECT) in one shot, or run just the highlighted text. ⌘+Enter auto-detects: a single statement keeps today's rich Table/Chart/EXPLAIN path; >1 statements run sequentially (one ClickHouse request each, stop-on-first-error) into a per-statement summary grid. - runScript in app.js: fresh query_id per statement (Cancel kills the live one), abort → stop + mark cancelled, one history entry for the whole script. - run() gains an opts.sql override so a single selected statement uses the rich path; runEntry routes the Run button + ⌘+Enter and picks script vs single. - Selection tracked via a hasSelection signal (selectionchange, gated on the editor being focused); the Run button flips to "Run selection". - Row-returning statements fetched as JSONCompact capped at 100 rows (max_result_rows / result_overflow_mode); Col 2 shows the first row comma-joined, click opens all rows in a side pane (reuses the cell-detail scaffold; shared Drawer deferred to #60). - state: hasSelection signal, recordScriptHistory, recordHistory sqlText override. Per-file coverage gate green (1078 tests); build OK.
- Truncation was undetectable: result_overflow_mode=break capped at exactly max_result_rows, so `data.length > cap` was never true and the "first N" badge never showed. Over-fetch by one (cap+1) so truncation is detectable. - leadingKeyword now skips a leading `(`, so `(SELECT …) UNION …` is classified row-returning instead of silently running as an effectful "OK" with rows hidden. - exportableResult() guards the script result shape (no rows/rawText) so a future Copy/Export binding can't TypeError on it. - runEntry no-ops when the split yields nothing (comment-/whitespace-only selection no longer POSTs a comment). - Single SELECT_ROW_CAP constant replaces the 100 literal repeated across the runner, parser, and grid label.
…to-run (#83) Follow-up UX fixes for multi-statement scripts: - Format now formats each statement via formatQuery() and rejoins them with `;` + a blank line (best-effort: an unformattable statement keeps its text). The Format button shows a busy spinner ("Formatting…") since it's one request per statement. Single-statement behavior (incl. syntax-error caret reveal) unchanged. - Explain (and the EXPLAIN view-switcher) show a clear toast — "Explain isn't available for a multi-statement script — run one statement at a time." — instead of sending `EXPLAIN a; b; …` and surfacing a confusing ClickHouse parse error. - Opening a saved query / history entry auto-runs ONLY read-only queries (new pure `isAutoRunnable` — every statement row-returning). An effectful query (CREATE/ALTER/DROP/INSERT/…) loads into the editor without executing, so a destructive statement is never run just by clicking it in the sidebar. Per-file coverage gate green (1093 tests); build OK; e2e green.
ed7d4e4 to
62a6096
Compare
…olumns (#83) - The script summary grid gains a 3rd column showing each statement's own execution time (right-aligned). The toolbar still shows the script total. - Grid columns are drag-resizable like the data table — generalized the existing resize helpers (applyFixedWidths/startColumnResize) with a keyOf mapper so the data grid keeps its row-number ('idx') column while the script grid keys by plain column index; no duplicated drag logic. - Initial column proportions 25% / 65% / 10% (Statement / Result / Time) via CSS; a drag then pins px widths, same as the data grid. Per-file coverage gate green (1097 tests); build OK; e2e green (39).
…sion (#83) ClickHouse's HTTP interface is stateless per request, so a multiquery script like CREATE TEMPORARY TABLE …; INSERT …; SELECT … couldn't see its own temp table across the separate requests. Each tab now gets a session_id (lazily generated) passed on its run + script requests with session_timeout=600, so session state — temporary tables, SET settings — survives across a script's statements and across successive runs in the tab. Schema/reference loads stay session-less (they fan out in parallel and would deadlock on the session lock); only one user query runs at a time, so the session is never contended.
…pane (#83) - Column resize now uses a splitter model: dragging a border trades width between the column and its right neighbor (total width and other columns stay put), fixing the shift/scroll artifact. The last column (no neighbor) still grows the table. Applies to the data grid, the script grid, and the rows pane. - Extracted renderGrid({columns,rows,sort,onSort,widths,onCell}) — one sortable + resizable table component. renderTable wires it to the global result/sort; the script-row side pane (openRowsViewer) now reuses it with local sort/width state, so the rows pane is sortable + resizable like the main grid (was a static table). - Resize helpers (applyFixedWidths/startColumnResize) now take a plain widths object, decoupled from the result. - Stacked drawers: only the topmost responds to Escape, so dismissing a cell drawer opened from the rows pane returns to the pane instead of closing both. - Confirmed -- / # / /* */ comment support end-to-end (added a script test). Per-file coverage gate green (1103 tests); build OK; e2e green (39).
…id fallback (#83) Multiquery scripts intermittently failed with "Network error". Cause: the script's statements share one ClickHouse HTTP session and run back-to-back, so a follow-up request can reach the server before it has released the per-session lock (released asynchronously, just after the prior response is flushed). Direct to ClickHouse that's a 500 SESSION_IS_LOCKED, but behind a proxy/LB it shows up as a reset connection → fetch TypeError → "Network error". Single queries issue one session request and never race, so they were stable. Fix: - runScript retries a statement ONCE on a transient failure — a connection reset (fetch TypeError) or a SESSION_IS_LOCKED response — after a short (env-injectable) delay, with a fresh query_id. Genuine query errors are not retried (stop-on-first). - Hardened the session_id / query_id fallback for non-secure (http://) contexts where crypto.randomUUID is undefined: mix in Math.random rather than only a coarse performance.now(), so two tabs can't generate the same id and collide on the session lock. Extracted a single uid(prefix) helper for all three id sites. Per-file coverage gate green (1107 tests); build OK; e2e green (39; 2 Firefox page.goto timeouts were load flakes, pass on re-run).
…ry (#83) Reworks the multiquery session model after research into the official @clickhouse/client-web (rejected: it can't control browser sockets so it wouldn't fix the keep-alive reset, its auth is static at client creation which breaks our per-request OAuth refresh seam, and it's a 4th runtime dep behind our injected fetch). Browser JS can't pin a TCP connection; ClickHouse sessions are logical (keyed by session_id), so the fix is to use them sparingly and retry safely. - Attach session_id ONLY when the SQL needs it (CREATE TEMPORARY / SET) or the tab already opened one (sticky, so temp tables / SET persist across runs). Ordinary scripts run session-less → no session-lock / affinity reset → no more intermittent "Network error" for the common case. - Idempotency-aware retry: SESSION_IS_LOCKED (rejected pre-execution) and a connection reset on a READ-ONLY statement retry once; a connection reset on an INSERT/DDL is NOT retried (it may have executed) and is surfaced as such. Per-file coverage gate green (1110 tests); build OK; e2e green (39).
…83) ClickHouse resets a session's idle timer when each query is *released* (end of the request) and cancels it while a query runs (Session.cpp / NamedSessionsStorage), so the countdown only ticks during true idle between queries. A script's statements are sent back-to-back (gap well under the 60s default), so the session never lapses mid-batch — no session_timeout override needed. Send only session_id and rely on the server default. Tests + build green (1110).
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).
BorisTyshkevich
added a commit
that referenced
this pull request
Jun 30, 2026
…blog post Cover the new multiquery + run-selection feature (#83/#95): a Features section ("Run a whole script, not one statement at a time") with a real capture of a 6-statement script + per-statement grid, a screenshots-gallery entry, and a blog post ("Run a whole SQL script, statement by statement") explaining the client-side split, the per-statement OK/preview/time grid, sessions for TEMPORARY/SET, and run-selection. Added to the blog index + cross-links. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PWop2ih7HortGgWwG23ahC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Closes #83 (Roadmap #68, Phase 2 — editor-intelligence track).
ClickHouse's HTTP interface runs exactly one statement per request, so pasting
CREATE …; INSERT …; SELECT …used to error out, and there was no way to run just a highlighted statement. This adds multiquery + run-selection.Behavior
;) behaves byte-for-byte as today (Table/JSON/Chart, EXPLAIN, FORMAT). More than one statement runs sequentially — one request each — into a compact per-statement summary grid.SELECT/WITH/SHOW/…) fetch asJSONCompactcapped at 100 rows; Col 2 shows the first row comma-separated, click opens all rows in a side pane. Effectful statements (DDL/INSERT) show OK. The whole script is recorded as one history entry on a clean run.Shape
src/core/sql-split.js(pure, 100%) — lexical;-splitter that skips literals ('…'/"…"/`…`, with\'and''escapes) and--/#//* */comments, plus anisRowReturningclassifier.src/core/script-result.js(pure, 100%) — parse aJSONCompactbody →{columns, rows, truncated}+ a first-row preview; oneSELECT_ROW_CAPconstant.src/net/ch-client.js— additiveo.paramspassthrough onrunQuery(own commit) so SELECTs can cap server-side.src/ui/app.js—runScriptrunner +run(opts.sql)override +runEntrydispatcher;hasSelectionsignal viaselectionchange.src/ui/results.js—r.scriptbranch + summary grid + side-pane rows viewer (reuses the cell-detail scaffold; a sharedDrawerstays deferred to Version-exact reference docs in the editor from ClickHouse 26.6 embedded docs #60).A
Plansubagent stress-tested the approach and a 3-angle/code-reviewpass ran before this PR; both folded in (notably: over-fetch by one so truncation is detectable, parenthesized-SELECTclassification,exportableResultguard, no-op on comment-only selection).Known limitation (documented): an
INSERT … FORMAT …with inline data containing;mis-splits — run those on their own.Checklist
npm testpasses (the per-file coverage gate is non-negotiable) — 1082 tests; new core modules at 100%npm run buildsucceeds (single-filedist/sql.html)src/core/, network insrc/net/(injected fetch), DOM insrc/ui/CHANGELOG.md([Unreleased]) updated — CHANGELOG updated (no deployed-surface/README change)Playwright e2e (Chromium/Firefox/WebKit) green. The run/results flow needs a live ClickHouse, so it's covered by unit tests rather than headless e2e.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LnuZaQGhGdQy7vNTvGXCQ2