Skip to content

fix(vscode-extension): ask before running a write, as the web editor does - #1990

Merged
cevheri merged 8 commits into
libredb:mainfrom
MarcBasas:fix/vscode-dangerous-query-confirmation
Nov 4, 2026
Merged

cevheri merged 8 commits into
libredb:mainfrom
MarcBasas:fix/vscode-dangerous-query-confirmation

Conversation

@MarcBasas

@MarcBasas MarcBasas commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

In the VS Code extension, DELETE FROM orders ran on the first click, while the web editor asks first. Run now asks the same question before it sends: the statement goes through the web editor's own isDangerousQuery, under the connection's type, and a flagged one opens a modal showWarningMessage that shows the statement and offers Run Anyway. Cancel, Escape and a cancelled run send nothing. A plain SELECT asks nothing.

isDangerousQuery lived in src/components/QuerySafetyDialog.tsx, and the extension's child bundle refuses anything under src/components (and the host bundle refuses src/lib/sql). So the gate moves unchanged to src/lib/sql/dangerous-query.ts (first commit, a pure move) and the child reads it. query/target already hands the host the statement Run will send, so it now also carries the gate's verdict dangerous for that statement, and the host asks unless the verdict is exactly false. No second round trip and no second copy of the gate.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test addition or update

Related Issue

Closes #1935

Changes Made

  • src/lib/sql/dangerous-query.ts: isDangerousQuery, writesUnderGrammar and their helpers, moved from QuerySafetyDialog.tsx unchanged. The component re-exports both names, so use-query-execution, use-query-adapter, the package exports and every test that mocks the component are untouched.
  • vscode-extension/src/protocol.ts, src/backend/handlers.ts: RunTargetResult.dangerous: boolean, required, set by runTarget from isDangerousQuery(statement, connection.type) on every answer (false for an empty target). PROTOCOL_VERSION stays 1 (host and child ship in one VSIX).
  • vscode-extension/src/host/editor/run-query.ts: after the target is read and before the request is built, a flagged statement asks in a modal. It fails closed: an answer without the verdict (a stale or swapped child bundle) asks too. The wait is raced against the run's cancellation token like the other waits in prepare.
  • docs/VSCODE_EXTENSION.md (step 4, the sequence diagram, the component table, and the "what differs" entry that said there was no confirmation), vscode-extension/README.md, and two docs/BACKLOG.md lines that named the old file.

Left out on purpose: the AI risk analysis in the web dialog, and the typed confirmation some engines require (an etcd range delete or lease revoke). The extension asks one question for those too. docs/VSCODE_EXTENSION.md says so. Select Rows and Load More ask nothing, as in the web editor.

Testing

  • I have tested this locally
  • I have added/updated tests
  • All existing tests pass

New tests:

  • test/unit/editor/run-query.test.ts: a flagged target shows the modal with the statement and runs only after Run Anyway; dismissing it, and cancelling the run during the question, send nothing and settle the run; an unflagged target shows nothing.
  • test/unit/backend-handlers.test.ts: query/target flags a DELETE, a write behind a read in one selection, a MongoDB deleteMany, leaves a SELECT and a find unflagged, and reads the same text differently under MySQL and PostgreSQL (# comment).
  • test/integration/sqlite-child.test.ts: the built dist/backend.js answers dangerous for a DELETE and not for a SELECT.

What I ran (Windows 11, Node 24.18.0, Bun 1.4.4):

  • bun run format, lint (0 errors), typecheck, knip, readme:check, security:check, channels:showcase:check, build, build:lib: pass.
  • vscode-extension: npm run typecheck, npm run build (bundle boundary checks pass), npm run coverage:check (100% lines), and the SQLite child integration test on the node runtime: pass. npm run test:unit passes except webview-shims.test.ts, which compares a POSIX path and fails on Windows, and two webview scroll tests that time out when the machine is busy with the root suite (they passed in the first run).
  • bun run test: 1719 of 1745 files pass, including every component, hook and SQL test around the gate. The 26 failing files are Windows-only (bun cannot pass a -e script through bun.cmd, tar cannot take a C: path, TLS and transport probes); none of them mentions the gate or the dialog.

Not run: chart:check, test:coverage / coverage:check at the repo root, the Docker-backed extension integration tests, the extension's e2e leg, and a manual run in a real VS Code window.

Test Environment

  • LibreDB Studio Version: main at fb54531, extension 0.4.0
  • Browser: none
  • OS: Windows 11
  • Node.js/Bun Version: Node 24.18.0, Bun 1.4.4
  • Database Type: SQLite (child integration test); the rest are unit tests with a fake provider

Checklist

  • My code follows the project's code style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation accordingly
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • The required CI test job passes the 100% line-coverage gate (bun run test:coverage and bun run coverage:check)
  • If I changed src/lib/db/providers/, I updated the matching docs/providers/ documentation and tests/integration/db/ tests in the same PR (provider triad)
  • If the type-id has a non-null AGENT_STATEMENT_LANGUAGE row, I kept its agent leg in step: the engine overlay under src/lib/agent/languages/<statement-language>/engines/, the Agent language pack subsection of its provider document, and tests/unit/lib/agent/engine-docs.test.ts
  • Any dependent changes have been merged and published

Maintainer update (2026-11-04)

I took this over to finish it; Marc's two commits are kept with his authorship, rebased on main without conflicts (the dialog had not changed on main, so the move is still verbatim).
Added on top:

  • The two integration tests from the review (results-view.test.ts PostgreSQL UPDATE, tree-editor.test.ts MongoDB non-JSON run) answer Run Anyway, restore the default in a finally, and assert the modal.
  • dangerous is a required boolean, and the host fails closed when it is missing, with a unit test that fails if the check is reverted.
  • Docs: which statements ask (a plain INSERT or CREATE does not; upsert and replace forms such as INSERT ... ON CONFLICT DO UPDATE, CREATE OR REPLACE TABLE and INSERT OVERWRITE do), where Select Rows differs from the web table click, and the gate's new module in docs/editor/query-optimization.md.

Run on Linux at the rebased head: the full root gate set, bun run test (2011 files), test:coverage plus coverage:check (100% lines), build:lib, attw; in vscode-extension, unit (1256 passing, 100% lines), integration on both the node and vscode runtimes against Docker (74 passing), e2e, and package.
This PR is also the first step of #2204, which builds its shared statement analysis on src/lib/sql/dangerous-query.ts.

Additional Notes

The gate moved without edits, so the existing tests/components/QuerySafetyDialog.test.tsx and the other gate tests keep covering it through the re-export. The root coverage gate is not checked locally; the moved file is exercised by the same tests as before.

@MarcBasas

Copy link
Copy Markdown
Contributor Author

@cevheri this is ready for review. It makes Run in the VS Code extension ask before sending a statement isDangerousQuery flags, with a modal showWarningMessage (statement shown, Run Anyway button).

The gate had to move out of QuerySafetyDialog.tsx (first commit, unchanged code, re-exported from the component) because the extension's child bundle refuses src/components. query/target now also answers dangerous: true, and the host asks on that, so the verdict is read once, in the child, under the connection's type.

To check it: cd vscode-extension && npm ci --ignore-scripts && npm run build && npm run test:unit && npm run coverage:check, plus LIBREDB_BACKEND_RUNTIME=node npx mocha --config test/integration/.mocharc.json --require test/setup/register.cjs --grep "SQLite in the backend child" for the built child. Manually: run DELETE FROM t on any connection, the modal appears and nothing is sent until Run Anyway.

Not run locally: a real VS Code window, the Docker-backed integration and e2e legs, chart:check and the root test:coverage gate (on Windows 26 root test files fail for spawn, tar and TLS reasons, none about the gate). The fork's workflow run needs your approval before CI can start.

One call you may want to weigh in on: the web dialog's AI risk analysis and the typed confirmation some engines use (etcd range delete, lease revoke) are not reproduced; the extension asks one question for those. docs/VSCODE_EXTENSION.md says so.

@codecov

codecov Bot commented Oct 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cevheri cevheri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Marc, the design is right and I want to merge it. One change first.

The required checks pass, but the VS Code Extension job is red here and green on main (run 40938036959). Two existing integration tests now run a statement your gate flags: the PostgreSQL UPDATE at results-view.test.ts:533 and the MongoDB non-JSON run at tree-editor.test.ts:907. The fake window (test/fakes/vscode.ts:47) answers any unscripted message with undefined, so your modal declines and nothing is sent. The failure at results-view.test.ts:603 follows from the first. The modal is right, the tests predate it.

Please script answerMessages(() => RUN_ANYWAY) in both tests and restore the default in a finally: the integration config does not load the unit leg's reset hook, and a leaked answer would change later tests. Also assert the modal itself, since these are the only tests where a real child's verdict reaches the host. That leg needs Docker, so CI can confirm it.

Reverting run-query.ts or handlers.ts turns your new tests red, good. Leaving out the AI analysis and etcd's typed confirmation is fine, the doc says so. If you want a follow-up for the etcd typed step, tell me and I will open the issue.

Done when:

  • results-view.test.ts, PostgreSQL UPDATE case: RUN_ANYWAY scripted, default restored in finally, exactly one modal warning whose detail is update it_update3 set n = n + 10.
  • tree-editor.test.ts, MongoDB non-JSON case: the same, one modal warning for { collection: orders } before the provider's error.
  • The VS Code Extension job is fully green on the new head (both integration legs, e2e, VSIX step, none skipped), required checks still green.
  • No production file changes beyond the current diff.

@cevheri

cevheri commented Nov 2, 2026

Copy link
Copy Markdown
Member

hi @MarcBasas
any update the CR

@cevheri

cevheri commented Nov 4, 2026

Copy link
Copy Markdown
Member

Thanks Marc. Since this has been quiet for a few days and #2204 builds on the gate move here, I'm taking this over: I'll rebase it on main, fix the two integration tests from my review, and push to this branch, keeping your commits. Your authorship stays on them.

MarcBasas and others added 8 commits November 4, 2026 15:06
isDangerousQuery and writesUnderGrammar lived in a "use client" component
file, which the VS Code extension's child cannot bundle (its boundary check
refuses src/components). They move unchanged to src/lib/sql/dangerous-query.ts
and the component re-exports both, so the hooks, the published package and
the tests that mock the component import them as before.
…does

Run sent the target straight to query/run, so DELETE FROM orders ran on the
first click while the web editor asks first. query/target now also answers
dangerous: true when isDangerousQuery flags the target under the connection's
type, and Run shows a modal warning with the statement and a Run Anyway button
before anything is sent. Cancel, Escape and a cancelled run send nothing.

Select Rows (a generated SELECT) and Load More (pages of a statement already
confirmed) ask nothing, as in the web editor. The AI risk analysis and the
typed confirmation some engines require are not reproduced.

Closes libredb#1935
… the gate now flags

The PostgreSQL UPDATE in results-view.test.ts and the MongoDB text that is
not JSON in tree-editor.test.ts are flagged by isDangerousQuery, so Run now
asks before sending them, and the fake window declines any message it was not
told how to answer. Both cases script Run Anyway, put the default answer back
in a finally (the integration config loads no reset hook), and assert the one
modal warning with the statement as its detail: these are the only tests where
a real child's verdict reaches the host.
…on leaves out

D409 now describes the gate in src/lib/sql/dangerous-query.ts with the
dialog's re-export, and keeps open what the move did not do: the tests, the
imports and the stub seams. The destructive-statement section names
isDangerousQuery and its module. The extension README says the question is
one modal dialog, without the web editor's AI risk analysis or etcd's typed
confirmation, as docs/VSCODE_EXTENSION.md already does.
…t, never leave it out

dangerous was optional and only ever present as true, so a child answer that
left it out read as safe and Run sent the statement without asking. It is now
a required boolean: the child answers isDangerousQuery's verdict on every
target, false on an empty one, and the compiler refuses a child path or a
test double that forgets it. The host still trusts its own bundled child's
typed answers, as it does for every other method, so no runtime check is
added and PROTOCOL_VERSION stays 1.
…s differs

The extension README named only writes to data, objects and permissions, but
the gate never asks on an INSERT or a CREATE and does ask on text it cannot
read safely, such as MongoDB text that is not JSON. It now lists what asks and
what does not. docs/VSCODE_EXTENSION.md said Select Rows runs a generated
SELECT; it runs whatever read the engine's generator built, and unlike the
web editor's table click it does not go through the gate, which the section
now says with the two names that ask on the web. The destructive-statement
paragraph in query-optimization.md puts each sentence on its own line.
…us is false

The host asked only on a truthy dangerous, so a child answer that left the
field out (a stale dist, a swapped backend) ran a write without the modal.
The required type checks the child's handlers, not the wire. Run now skips
the question only when dangerous is false exactly, and the protocol docblock
says what the compiler does and does not hold.
…ows example

The extension README said an INSERT or a CREATE never asks, but the gate asks
on PostgreSQL INSERT ... ON CONFLICT DO UPDATE, Snowflake CREATE OR REPLACE
TABLE and Spark INSERT OVERWRITE TABLE, each checked against isDangerousQuery.
docs/VSCODE_EXTENSION.md named an S3 bucket with -- as a Select Rows read the
web editor asks on; the generated aws s3 ls for such a bucket does not ask, so
only the measured ClickHouse a$$b example stays.
@cevheri
cevheri force-pushed the fix/vscode-dangerous-query-confirmation branch from dcbeb91 to 122edd3 Compare November 4, 2026 12:53
@cevheri
cevheri merged commit 3d84867 into libredb:main Nov 4, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] VS Code extension runs a write without the confirmation dialog the web editor shows

2 participants