Skip to content

feat: add full query text support to run with selection option - #604

Open
emrberk wants to merge 12 commits into
mainfrom
feat/run-selection-full-query
Open

feat: add full query text support to run with selection option#604
emrberk wants to merge 12 commits into
mainfrom
feat/run-selection-full-query

Conversation

@emrberk

@emrberk emrberk commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

Adds a new option Complete queries to run full query texts in the selected range.

Example

we have

select amount from core_price where symbol = 'EURUSD'; select 2;|

and the selected portion is:

core_price where symbol = 'EURUSD'; select 2;|

Before

  • Off: Selection is ignored. Only the query in cursor, that is select 2, runs.
  • On: The selection is run verbatim, taking only the selected portion of the queries. In this case, core_price where symbol = 'EURUSD' and select 2.

Now

  • Off: Same behavior as the previous "Off"
  • Partial queries: Same behavior as the previous "On"
  • Complete queries: select amount from core_price where symbol = 'EURUSD' and select 2 runs
Screenshot 2026-08-27 at 17 27 53

@emrberk

emrberk commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

Review — PR #604 feat: add full query text support to run with selection option

Reviewed at level 3 (full mission-critical pass).

  • Base: ca336b047623a142cd57a855729e60bd049a3131 (main, release: 2.0.3)
  • Head: 1d1b151a2f7e2905d744ca4c934db73a6908f9dd
  • 22 files, +1881 / −334

PR title and description

Title follows Conventional Commits. The description states end-user impact and
gives a before/after example per mode. No change requested.

Quality checks

Check Result
yarn typecheck pass
yarn lint (eslint + color tokens) pass
yarn test:unit pass — 89 files, 2025 tests
yarn build pass

Issues

# Issue Category Severity Location Net impact Evidence
1 Notebook Cmd+Enter dead for wrapper focus Accessibility & UX Moderate in-diff Notebook users clicking anywhere non-interactive N/A — static, Cell.tsx:385-388 + useCellWrapperInteractions.ts:95-96
2 Run clears the selection in Complete mode Accessibility & UX Moderate in-diff Complete-mode users lose the selection every run N/A — static, index.tsx:474-486 vs base
3 Downgrade reads every new setting value as off Persistence & migrations Moderate in-diff Users who roll a QuestDB server back after saving settings N/A — static, utils.ts:32-39 vs base parseBoolean
4 Staleness guard written three times Code structure, readability & types Minor in-diff Maintainers of the repo's largest file N/A — static, index.tsx:680-684 / 707-711 / 1708-1712
5 New tests decouple the cursor from the selection Test review & coverage Minor in-diff Weaker regression protection than it appears N/A — static, utils.test.ts:109, notebookUtils.test.ts:1361

#1 — Notebook Cmd+Enter is a no-op whenever focus is on the cell wrapper

Description

The cell's window keydown handler (Cell.tsx:373-392) used to run the cell for
any Cmd/Ctrl+Enter that reached it — that is, whenever the cell was focused and
focus was not inside Monaco, which has its own action. The new version handles
Shift+Enter, then handles the non-shift case only when focus is inside
resultRef. There is no else, so every other focus location is a silent
no-op, and preventDefault is not called either.

The gate misses the very case it was written for. useCellWrapperInteractions.ts:95-96
focuses wrapperRef on any mousedown whose target does not match
"button, a, input, select, textarea, [contenteditable], .monaco-editor", while
canArrowMove is true (layoutMode !== "grid" && !isMaximized, line 32).
wrapperRef is the ancestor of resultRef, so resultRef.contains(wrapper)
is false. Clicking non-interactive content inside the result area — the
status-notification text, the padding around the grid — therefore lands focus on
the wrapper and kills the shortcut.

Reachable focus states where base ran the cell and head does nothing:

  • the cell wrapper, after clicking the result's notification text, the cell
    padding, the drag header, or a divider;
  • a toolbar button, after clicking Run cell / a view toggle / refresh;
  • the cell-name input;
  • document.body, in grid layout mode (canArrowMove false, so nothing takes
    focus).

A view-maximized cell is the worst case. Cell.tsx:504 renders
EditorContainer only when !isViewMaximized, so in the grid-only view Monaco
is not mounted at all and its own notebook-run action does not exist. Every
Cmd+Enter in such a cell goes through this handler, and it fires only when focus
is literally inside the result DOM — which the wrapper-focus behaviour above
prevents for most clicks.

Cmd+Shift+Enter still works from every location, and the Monaco and result-tab
paths work, so the flow is recoverable — but the primary run shortcut silently
stops working with no toast and no other feedback.

Steps to reproduce

  • Open a notebook cell with select 1; and run it.
  • Click the result's status-notification text (or the padding beside the grid).
  • Press Cmd/Ctrl+Enter.
  • Nothing happens. At base the cursor's statement ran.

Suggested fix

Give the non-shift branch an explicit fallback so the resolver stays reachable,
keeping the intended result-area routing:

e.preventDefault()
if (resultRef.current?.contains(document.activeElement)) runSingleFromResult()
else runSingleFromEditor()

Add runSingleFromEditor to the effect's dependency array. If the no-op is
deliberate, the wrapper-focus case still needs handling, because clicking inside
the result slot does not put focus inside resultRef.


#2 — Running in Complete mode clears the user's selection

Description

handlePrimaryRun resolves a Complete-mode selection into a whole-statement
Request with no selection field. runQueryAction then calls
setCursorBeforeRunning, whose new guard (index.tsx:474-478) takes the "leave
the cursor alone" fast path only when the mode is off or the selection is
empty. In Complete mode with a live selection it falls through to
editor.setPosition(...) (index.tsx:484), which collapses the selection to the
statement start.

The correct SQL still runs — the request is captured in pendingQueryRequestRef
before the cursor moves — so this is not a data problem. The cost is
interaction: in Complete mode the selection the user made to express intent is
destroyed by every single-statement run, so re-running the same fragment needs a
fresh selection, and the run button label flips back to "Run query". Partial mode
keeps the selection (its requests carry selection, so the range is re-applied),
so the two "on" modes behave inconsistently.

The same guard also changes base behaviour in the default Partial mode: running a
whole statement from the glyph dropdown while a fragment is selected used to hit
the fast path (the cursor query and the glyph query produce the same key) and
preserve the selection; it now collapses it.

There is no regression test for selection preservation on any run path —
grep -rn "getSelection()\|preserveSelection" e2e/tests/console/editor.spec.js
returns notebook-only hits — even though commit cda7c901 ("preserve selection
on query run in off mode") treats it as a fix.

Steps to reproduce

  • Editor Settings → Run with selectionComplete queries → Save.
  • Type select a from trades where symbol = 'X'.
  • Select only the table name trades.
  • Press Cmd/Ctrl+Enter, or click Run query.
  • The whole statement runs, but the trades selection is gone and the caret sits
    at the start of the statement.

Partial-mode (base regression) variant:

  • Leave the mode at the default Partial queries.
  • Type select a from trades; select 2; and select trades.
  • Click the run glyph on line 1 and choose the whole-query entry.
  • At base the trades selection stayed; on this branch it is gone.

Suggested fix

The cursor move is no longer needed for correctness on these paths —
pendingQueryRequestRef carries the exact request, and getQueryStartOffset
derives its offset from the request, not the cursor. Pass preserveSelection
through when the run came from a resolved selection:

if (queries.length === 1) {
  handleRunQuery(queries[0], resolvesSelection)
}

That leaves off-mode and no-selection behaviour untouched and lets the extra
mode condition come back out of setCursorBeforeRunning. Add an e2e assertion
that the selection survives a run in each mode.


#3 — After this ships, an older console reads every stored value as "off"

Description

The setting keeps its localStorage key editor.runWithSelection
(StoreKey.RUN_WITH_SELECTION) while its value domain changes from
"true"/"false" to "partial"/"complete"/"off". The upgrade direction is
handled — parseRunWithSelectionMode (utils.ts:32-39) maps "false"off
and everything else, including the legacy "true", to partial, which I checked
is behaviour-preserving against the base resolver.

The downgrade direction is not. Older code reads the key through
parseBoolean(value, true) = value === "true", so all three new values —
including "partial", which is exactly the old ON default — read as false. A
user who never turned the feature off finds it silently off, and pressing Save on
the old build then persists "false", so re-upgrading lands on off
permanently.

This is reachable because console versions are pinned to server versions:
.github/workflows/release_web_console.yml publishes @questdb/web-console to
npm, then its update-questdb job writes that version into questdb/questdb's
pom.xml. One console bundle ships per server binary, served from the same
origin against the same localStorage, so a server rollback serves the older
console. The producer needs nothing unusual: EditorSettingsModal:167 writes the
key unconditionally on Save, even when the user only edited Max column width.

No data is lost and the direction is conservative (the editor runs the cursor
statement rather than more SQL), and it is two clicks to fix, which is why this is
not Critical.

Steps to reproduce

  • On a build with this PR, open Editor Settings, change anything, press Save
    (editor.runWithSelection is now "partial").
  • Roll the QuestDB server back to a version whose embedded console predates this
    PR, and open the console on the same host and port.
  • Editor Settings shows Run with selection off; selections are ignored.

Suggested fix

Either write the enum to a new key (editor.runWithSelectionMode) and keep
reading the old one as the legacy fallback, or keep a legacy-compatible mirror —
write "true" to editor.runWithSelection for partial/complete and
"false" for off alongside the enum — so an older reader degrades to the
nearest old behaviour instead of always to off.


#4 — The run-plan staleness guard is written three times

Description

The same three-condition check — active buffer id, model identity, model version
id — appears verbatim at index.tsx:680-684 (runQueryAction's execute),
707-711 (executePendingScriptRun) and 1708-1712 (handleRunScript), each
followed by a toast.error whose two message variants differ only in
"query"/"queries". createScriptRunPlan already exists to build the captured
plan; the matching predicate does not. A fourth run entry point will silently
skip the guard, and this is the largest file in the repo.

Steps to reproduce

  • grep -n "modelVersionId" src/scenes/Editor/Monaco/index.tsx

Suggested fix

Add a companion to createScriptRunPlan and call it at all three sites:

const isRunPlanStale = (plan: Pick<ScriptRunPlan, "bufferId" | "model" | "modelVersionId">) =>
  activeBufferRef.current.id !== plan.bufferId ||
  editorRef.current?.getModel() !== plan.model ||
  plan.model.getVersionId() !== plan.modelVersionId

runQueryAction can build the same shape for a single query instead of keeping
three loose locals.


#5 — The new unit tests model states the code under test cannot produce

Description

Two of the test helpers this PR adds weaken the assertions built on them:

  • makeMultiLineEditor (utils.test.ts:109) hardcodes
    getPosition: () => getPositionAt(text.length) — the cursor is always at the
    end of the document, independent of the selection. In Monaco the cursor is one
    end of the selection. Every off-mode assertion in a multi-line test therefore
    resolves to the last statement regardless of where the selection sits, so those
    cases pin a state a real editor cannot reach and do not protect the behaviour
    they name. makeSingleLineEditor takes cursorColumn as an independent
    argument with the same effect.
  • heightForResult (notebookUtils.test.ts:1361) synthesizes the new value
    argument from the results themselves, guaranteeing a 1:1 statement frame. The
    whole point of the new parameter is that value and results can diverge, so
    the nine migrated assertions exercise none of it. Only the three
    hand-written-value tests cover the new behaviour, and none reaches the
    ?? derivePositionalFrame or if (!frame) fallbacks.

The offset and range arithmetic in both stubs is faithful — this is a cursor and
input-fidelity problem, not a broken harness — and the e2e suite covers off-mode
behaviour against real Monaco, which is why this is Minor.

Steps to reproduce

  • Read src/scenes/Editor/Monaco/utils.test.ts:100-127 and
    src/scenes/Editor/Notebook/notebookUtils.test.ts:1361-1366.

Suggested fix

Derive getPosition() from the selection's end in both stubs (and let a test opt
into a backward selection explicitly), and drop heightForResult in favour of
passing the cell value each assertion actually means.


Adjacent findings (not blocking — file as issues)

A multi-statement selection run skips the destructive-action confirmation

  • Problem: Selection script runs bypass the "may delete data" dialog
  • Net impact: All console users; selected DDL/DML runs with no confirmation
  • Location: src/scenes/Editor/Monaco/index.tsx:1636-1647 (base: 1553-1566)
  • Symptom: Selecting several statements that include DROP / DELETE /
    TRUNCATE and pressing Cmd+Enter executes them immediately. "Run all queries"
    shows a confirmation for the same SQL; a selection run does not. The dialog
    appears on this path only when another execution is already active, and then
    only to warn that it will be aborted.
  • Reachability: Idle editor, selection spanning more than one statement,
    Cmd+Enter or the Run selected queries button.
  • Suggested fix: Show the same confirmation when a selection resolves to more
    than one statement, or at least when any resolved statement classifies as a
    write.
  • Severity if filed standalone: Moderate
  • Note: identical at base (runsAllQueries was false for a multi-query
    selection there too), so it is not attributed to this PR. Worth filing because
    this PR widens what a selection reaches, in two ways: Complete mode expands a
    one-character overlap into the whole statement (I reproduced
    COMPLETE ["SELECT 1","DROP TABLE t"] from a selection touching one character
    of DROP), and handlePrimaryRun now resolves from a full-document parse
    instead of the viewport-windowed offsets, so Cmd+A in a long buffer runs every
    statement rather than the visible window. Both are correct on their own terms —
    the second fixes a real base bug — but they land on a path with no confirmation.

Summary

Verdict: approve with comments.

Address #1 and #2 before merge if you agree they are unintended; neither blocks.
#3 is worth a decision now because it is only cheap to fix before the enum ships.

  • Correctness gate: passes. No admitted Critical. The change is largely a set
    of corrections to previously broken selection handling, and the base
    comparisons back that up: a comment-only selection used to invert the
    first/last offset lookup and could target a neighbouring statement (now []);
    handleRunScript used to fall back to run-all when the selection ref held one
    entry (now an explicit plan); a selection run started during a script used to be
    discarded (now queued behind a confirmation, with a new test); Alt+L used to
    copy syntactically broken SQL such as SELECT; (now the whole statement); a
    share link whose text matched mid-statement used to select and run that
    fragment (now it opens its own buffer); and Cmd+Enter used to resolve against
    viewport-windowed offsets, so a selection past the window ran the wrong set.
  • Test gate: passes. Zero admitted Critical coverage gaps. Coverage for the
    new pure logic is real and the e2e suite exercises all three modes in the editor
    and in notebooks, the legacy "false" migration on boot, the queued selection
    run, comment-only and separator-only selections, and share-link fragment
    isolation. Two Moderate gaps are noted inside findings rather than as separate
    rows: no regression test for selection preservation (init react components package #2) and no test for the
    new value argument's divergent case (Console autocomplete tests #5). The three staleness guards are
    untested but unreachable from any production path traced, so they are defensive
    rather than functional.
  • In-diff / out-of-diff split: 5 in-diff, 0 out-of-diff. The 2.5d exposure
    list was walked callsite by callsite. The consumers that could have broken did
    not: getDropdownQueries, applyLineMarkings, createQueryKeyFromRequest,
    getQueryStartOffset, the error-marker offset shift, extractErrorByQueryKey
    and QueryDropdown.isExplainDisabled all tolerate Request.selection being
    absent in Complete mode; ButtonBar handles both the empty queriesToRun and
    the missing selection; all six computeResultBottomHeight callsites pass a
    value from the same store snapshot the renderer reads; and the Run button now
    joins the managed execution path it previously bypassed, which
    queryExecutionManager's own comment names as the intent. The change is
    genuinely well contained.
  • Severity distribution: 0 Critical, 3 Moderate, 2 Minor.
  • Migration: the "true""partial" upgrade is behaviour-preserving. Base
    runWithSelection === true and head "partial" differ only where the two new
    guards fire (whitespace/comment-only selections and the inverted-offset case),
    and base already returned [] for the first and mis-targeted a statement for
    the second. The e2e comment asserting this is correct. The downgrade direction
    is disabled buttons #3.
  • Not a finding, but sanity-check it — the notebook "Nothing to run" toast.
    handleRunSingle("editor") dropped both of its fallbacks
    (useCellRunActions.ts:186-194): base did
    sql = cursorQuery ?? resolveActiveStatementSql(...) and then
    if (!sql?.trim()) await handleRunAll(). This is a deliberate, tested
    decision — the e2e test "never widens notebook Cmd+Enter beyond its focused
    query"
    asserts the toast, and one of its steps is commented "the same active
    tab never becomes a fallback for Monaco" — so it is not reported as a defect.
    Worth a second look anyway, because the tests cover only a comment-line
    cursor, and I confirmed getQueryFromCursor also returns undefined for the
    trailing blank line after a semicolon-terminated statement: type
    select 1;, press Enter, press Cmd+Enter, and a single-statement cell now
    errors where base ran it. The "never widens" rationale does not apply there —
    handleRunAll() would have run exactly the one statement the user wrote.
    ("select 1\n" without the semicolon still resolves, so only the terminated
    form is affected.)
  • Not a finding — checked and dropped: the e2e/questdb submodule bump
    (9b59a921 → e40ec59d, 15 commits) is required, not scope creep:
    .github/workflows/check_oss_submodule.yml runs on every push and
    submodule_is_mergeable.py fails the build when the pointer is more than
    ACCEPTABLE_LAG = 5 commits behind questdb master. The old pointer was 15
    behind. Two of those commits do change SQL semantics
    (fix(sql): … HORIZON JOIN, WINDOW JOIN, LATEST ON …, perf(sql): stream ordered UNION ALL without sorting) and union all appears in this file's
    share-link test, so an e2e failure here is not attributable to the UI diff
    alone — but that is the repo's standing policy, not this PR's choice.

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