Last updated: 2026-10-08
Visual Query Editor
Open any SQL statement as a structural diagram and edit it round-trip. The Visual Query Editor renders SELECT, INSERT, UPDATE, DELETE, and MERGE shapes as a stack of clause cards — joins, CTEs (including recursive ones), subqueries, set operators, and window functions all become navigable structure — and the Add buttons, inline pills, and chooser popovers write changes back into the same SQL buffer, byte-precise and undoable. You can run the statement without leaving the tab, too.
Why a structural view?
Long statements bury their structure. A 60-line SELECT with three CTEs, two outer joins, a window function, and a UNION branch is hard to read line-by-line — but the shape is small once it is laid out as boxes and arrows. The Visual Query Editor flips a buffer of SQL into that shape so you can:
- See every JOIN partner at a glance, including
USING/ONconditions and join kind (INNER, LEFT, RIGHT, FULL, CROSS, CROSS APPLY, OUTER APPLY, LATERAL). - Walk a CTE chain top-down: each CTE renders its own card and links into the body that consumes it.
- Spot subqueries in
FROMandWHEREas expandable cards rather than nested parentheses. - Read a UNION / INTERSECT / EXCEPT pipeline as labeled branches with the operator between each one.
- Navigate INSERT / UPDATE / DELETE / MERGE shapes with the same structure — column maps, SET assignments, WHEN-MATCHED branches, and so on.
Edits still happen in your Monaco buffer. VQE is the structural lens; the buffer is the source of truth.
When to use VQE — and when not to
The Visual Query Editor is most useful when you are reading SQL someone else wrote, or coming back to your own large query after a few days. It is less useful when you are mid-typing — Jam SQL Studio is a text-first IDE; VQE is the read lens.
- Use it for: reviewing a long SELECT, exploring an unfamiliar query in a notebook, understanding a procedure body's main statement, walking through a complex MERGE, picking which CTE to comment out.
- Skip it for: writing a new query from scratch, quick edits to a one-line statement, anything that does not benefit from a structural map.
The Visual Query Editor is on by default and still in Beta (since 1.5.3). It provides the SQL | Visual switch on the Query Editor toolbar, the Visual Query Editor entry in the main toolbar's More menu, the Cmd/Ctrl+Shift+V shortcut, the command-palette entry, and the pencil affordance in the Query Editor. To take them out of the way, untick it in Settings → Beta Features.
Opening the Visual Query Editor
You can reach VQE four ways: the SQL | Visual switch on the Query Editor toolbar, the Cmd/Ctrl+Shift+V shortcut and the Command Palette (all three switch the tab you are in; More → Visual Query Editor on the main toolbar does the same from anywhere), and the on-demand edit affordance under your cursor (the gutter pencil or the run-bar Edit button → the compact editor popover, with an Open full editor hand-off to the Visual view). Each one shows the same statement; the difference is just the entry surface.
Query Editor toolbar — SQL | Visual
The fastest entry point. The Query Editor toolbar has a two-part SQL | Visual switch next to Format and Optimize (icons only; the tooltips read SQL view and Visual view). Choose Visual and the same tab shows the statement under the cursor as clause cards — there is no second tab and no copy of your SQL. Both views show one document: the SQL editor stays open underneath with its text and undo history, every visual edit is written into that text, and choosing SQL brings the editor back with the statement you were looking at selected. A multi-statement buffer shows the statement picker (Statement 2 of 5) at the top of the Visual view; the view stays on its statement while you edit, undo, or format.
- Running the statement. While the Visual view shows, the toolbar's Execute button reads Statement and runs the statement you are looking at — as do F5 and Cmd/Ctrl+Enter — never a selection left behind in the hidden editor. Shift+F5 still runs the whole script. Results appear in the tab's usual results pane, with a notice when they are from an earlier version of the statement. Stop, the confirmation for destructive statements, and manual or smart transaction mode work exactly as in the SQL view, and switching views never re-runs or interrupts a query. For a SELECT, the view's header has a Run on change switch that re-runs the statement after each visual edit (see Run your query).
- Undo. Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z step through the Query Editor's own history, so typing you did in the SQL view and edits you made in the Visual view undo in one order.
- Connection and database are the tab's own, chosen in the status bar; the Visual view's Choose database actions open that picker.
- Saving. A tab tied to a
.sqlfile saves the edited SQL with Cmd/Ctrl+S from either view. Format is SQL-view only. - The tab remembers its view and statement across restarts. A separate Visual Query Editor tab saved by an earlier version opens as a Query tab in its Visual view, on the same connection and — when its saved position still falls inside its SQL — on the statement it showed; it stays its own tab and is never merged into the Query tab it was opened from. If its SQL was too large to keep, it opens in the SQL view with the comment explaining that instead.
The main toolbar has a Visual Query Editor entry under More. It shows the Visual view of the active Query Editor (a tab already in its Visual view stays on its statement), or opens a new query tab in the Visual view when no Query Editor is in front. You can promote it to the main toolbar row in Settings → UI → Toolbar.

If no Query Editor is active (for example, when you are sitting on the start page), More → Visual Query Editor, the shortcut and the Command Palette open a new query tab in its Visual view, with a starter canvas instead of an empty buffer: a searchable Start from a table picker, grouped by schema, and a Start from SQL button as the escape hatch. Pick a table and Jam seeds the buffer with SELECT * FROM <table> — a real, executable query from the moment it lands (one undo step) — and switches straight into the structural renderer; click Start from SQL instead to switch the tab to its SQL view and type your own statement. The table picker stays disabled, with an explanatory tooltip, until a connection is bound and its schema snapshot has loaded. A database with no tables says so on the canvas, with a Reload schema button (for tables created since the schema loaded) and a Choose another database button, which opens the tab's database picker in the status bar (SQLite, with one database per file, gets Reload schema only). If the table list could not be loaded, the canvas says so — Tables could not be loaded — reload the schema or start from SQL. — and shows only Start from SQL; the banner above it carries Reload schema.
Edit right where your cursor sits — the compact editor
Inside any Query Editor, an edit affordance appears for the SELECT / INSERT / UPDATE / DELETE / MERGE statement under your cursor. Its form follows the existing Inline Execute/Peek bar editor setting:
- Setting off (the default) — a small gutter pencil icon shows up in Monaco's glyph margin, to the left of the line numbers, with the tooltip "Edit this statement visually". It renders for the cursor statement in every buffer, single- or multi-statement. When the Execute / Peek gutter buttons also render (those appear only in buffers with 2+ statements), the pencil stacks below them.
- Setting on — a small pencil-icon Edit button rides on the floating Execute / Peek run bar.
- With text selected — when the selection is exactly one whole statement (with or without its
;), the pencil stays: in the gutter below the selection's Execute / Peek, and as an Edit button on the selection's run bar. It edits that whole statement. A partial selection has no pencil, because Execute and Peek run only the selected text.
Clicking the pencil (or the Edit button) opens the compact Visual Query Editor popover, anchored right at the cursor statement and running over the same byte-precise edit pipeline as the Visual view. The popover shows a summary line (e.g. "SELECT …") plus the clause cards; it covers the full SELECT shape inline — columns, DISTINCT, WHERE, joins, GROUP BY, HAVING (aggregate templates included), ORDER BY, and LIMIT, plus any WITH clause the statement has (see below) — and each edit writes back into the SQL buffer. Press Esc to close it.
On an UPDATE or DELETE the header names the target table, and the name is a picker: click it (the chevron marks it) to point the statement at another table, the same edit as Change table in the Visual view. The SET list below it has the same column pickers (see DML edits). When the statement names its target through an alias (UPDATE p … FROM Production.Product p) or before FROM (DELETE p FROM …), the name stays plain text; change it with Edit as SQL. INSERT and MERGE open in the Visual view (Open full editor).
The popover also edits WITH (CTE) queries directly: when a SELECT statement under your cursor has a WITH clause, a With strip renders above the clause cards, listing each CTE as a small chip (the strip reads With (recursive) when the clause uses WITH RECURSIVE, and a recursive binding's own chip carries a "recursive" label). Click a chip to drill into that CTE's own body — its columns, WHERE, joins, GROUP BY, and the rest of the clause cards render there, fully editable, the same way drilling into a subquery works. A CTE whose body isn't a plain SELECT (a data-modifying CTE, e.g. WITH d AS (DELETE … RETURNING …) SELECT …) shows its chip disabled with a tooltip explaining why, with Edit as SQL as the escape hatch. A recursive CTE's body is drillable, but since its shape is an anchor/recursive-step pipeline rather than a single SELECT, drilling into it shows the same degrade notice as any other structural shape the compact surface can't edit inline, with an Open full editor hand-off right inside the drill. Add CTE lives on the strip's own footer once a WITH clause exists; for a statement with none yet, Add CTE sits on the FROM card's action line, after Add join and Add cross-join, the same place as in the Visual view. The main query's own clause cards stay fully editable alongside the strip throughout. UPDATE and DELETE statements with a WITH clause still route to the Visual view (Open full editor).
The popover's Open full editor button switches the same tab to its Visual view on the exact statement under your cursor — in a multi-statement buffer that is the statement you were editing, not statement #1 — and, when you had drilled into a subquery in the popover, the Visual view opens on that same subquery.

DDL statements get their own compact treatment: instead of a clause-card stack, the popover shows a read-only summary card — a kind chip (CREATE TABLE, ALTER TABLE, DROP, and so on), the schema-qualified object name, and a few parsed facts (a column count for a CREATE TABLE, the columns an ALTER TABLE adds or drops, and similar per-kind detail) — plus a Design table button and a Close button (the statement is already in the SQL editor, so Close just closes the card). Design table opens the Table Designer in create mode from a CREATE TABLE card or alter mode from an ALTER TABLE card, and is disabled with a tooltip when no connection is bound. The summary card covers CREATE, ALTER, DROP, TRUNCATE, RENAME, GRANT, and REVOKE statements. Shapes VQE cannot diagram at all (procedural bodies, parse errors, and DDL-adjacent statements like EXPLAIN or SET) fall back to a read-only view with an Edit as SQL escape hatch, which is the universal way back to the raw buffer.
Start from a table — the "Query a table" glyph
When the cursor sits on a blank line in a Query Editor (outside every statement), the same gutter slot the pencil uses shows a small table-with-plus glyph instead, with the tooltip "Query a table…"; with the Inline Execute/Peek bar setting on it is a small Query a table pill at the caret. On an empty tab a grey hint next to it reads "Query a table, or start typing". Both open a searchable picker of the tables and views in the tab's database (grouped by schema, with a Recent group for tables you seeded earlier in this session once the database has more than 15 tables and views — on a shorter list every table is already in view — and a view badge on views). The picker's footer previews the exact statement a pick will write — SELECT TOP 1000 * FROM dbo.Orders; on SQL Server, LIMIT 1000 or FETCH FIRST 1000 ROWS ONLY on the other engines, the same row cap as the Object Explorer's Select top 1000 rows. If the table list can't be read (for example, your login can't open the database), the picker shows the database's error message and a Retry button.
Enter writes the statement at the caret, runs it, and opens the compact editor on it, so a WHERE, an ORDER BY, or a narrower column list is one click away. Shift+Enter writes and runs it without opening the editor. The statement is plain SQL in your buffer, so you can keep typing into it; the seed is one undo step. The Command Palette's Query a table… entry opens the same picker for the active Query Editor. The glyph follows the Visual Query Editor beta switch.
Two compact-editor conveniences make the seeded query behave like the Table Explorer:
- Choose columns — a starred
SELECT *shows a Choose columns button in the Select card, as its only add action. It replaces the*with the explicit column list of the statement's own tables (each instance of a self-join under its own alias, never an enclosing query's tables), so each column becomes a chip you can remove or reorder; over aNATURALorUSINGjoin it writes the columns your database's own*returns, in the same order, with each shared column once — without a table name when that is unambiguous, otherwise from the side it comes from (the right table for a RIGHT JOIN,COALESCE(…)for a FULL JOIN). On Oracle, which cannot put a table name on a shared column, it is disabled when that column's name is also another table's; on MySQL it is disabled for aFULL JOIN, which MySQL does not have; Add column appears once the list is explicit. - Run on change — a switch in the compact editor's footer, next to Done (on by default; also under Settings → UI → Visual editor, and the same setting as the Visual view's Run on change switch) re-runs the
SELECTafter every committed edit here, so the results grid follows the query as you add filters, sorts, and columns. It only ever re-runsSELECTstatements —INSERT,UPDATE,DELETE, andMERGEare never run automatically. - Automatic execution also refuses recognized side-effecting calls such as MySQL
GET_LOCK(), including whenNO_BACKSLASH_ESCAPESorANSI_QUOTESchanges how quotes are interpreted. Review such a query and run it manually when intended.
Keyboard shortcut — Cmd/Ctrl+Shift+V
Press Cmd+Shift+V on macOS or Ctrl+Shift+V on Windows / Linux to switch the active Query Editor between its SQL and Visual views; with no Query Editor in front it opens a new query tab in the Visual view (the starter canvas above). The shortcut is also listed on the keyboard shortcuts page.
Command Palette
Open the Command Palette (Cmd+Shift+P on macOS, Ctrl+Shift+P on Windows / Linux) and run Toggle Visual view. It does what the shortcut does: switches the active Query Editor between its SQL and Visual views, or opens a new query tab in the Visual view when there is none.
Reading a structural diagram
Every render mode lays out the statement as a vertical stack of clause cards, in SQL order. Each clause — FROM, SELECT, WHERE, GROUP BY, HAVING, ORDER BY and LIMIT — is one row: the keyword on the left and the pills beside it, with the Add buttons (disabled until editing is available) after the last pill, so a clause that fits on one line takes one line and the buttons move to the next line when there is no room; ORDER BY's sort columns sit side by side. Clauses the SELECT doesn't use yet share a single dashed row at the end — for example Add · + Where · + Group by · + Order by · + Limit (+ Having appears once there is a GROUP BY, and the limit entry reads Top or Fetch first on SQL Server and Oracle); picking one adds that clause, which then shows in its place in the stack. A SELECT without a FROM shows FROM — none · Add table, which adds the FROM clause with the table you pick. The compact popover uses the same rows. The cards are scoped to the current statement only — multi-statement buffers show a statement picker at the top (Statement 2 of 3 with a chevron; click it to switch). A single statement shows a plain Statement 1 of 1 label, and a blank buffer shows no label at all. In a Query tab's Visual view the connection and database are the tab's own, shown and chosen in its status bar. The compact editor's header shows the same connection name; its database is the Query Editor's, chosen in the status bar.
In the statement picker's list, each statement shows as its own text with its first keyword in bold. The engine appears as the same icon the Object Explorer uses for the connection. In the compact editor's header the engine name is left out, since the icon already shows it, and when space is short the database name keeps its width before the connection name does; hover the header to see the connection, host, engine and database in full.
SELECT — the canonical shape
Jam renders cards source-first — in the order the engine actually evaluates the statement, not the order you type it in. A SELECT can render up to nine cards, most of them only when the buffer actually has that clause:
- CTEs — one card per CTE, when the statement has a
WITHclause. Rendered above everything else; see below. - FROM — one item per from-source. Tables render as schema-qualified pills; subqueries render as expandable cards; the section is a drop zone — drag a table or view from the Object Explorer directly into FROM to add it.
- SELECT list — one pill per projected column. Each pill shows the expression, optional alias (
AS bb), inline-rename affordance, and a remove (×) button; the whole pill is the drag handle for reordering. A DISTINCT toggle chip follows Add column (outlined when off, filled when on). - WHERE — top-level boolean fragments split by
AND/OR. Each fragment is a pill with click-to-edit text and remove. The AND ▾ / OR ▾ chip between two conditions is a dropdown: pick the other connector to switch it (every connector of that group changes together). To negate a whole AND / OR group withNOT, switch the card to the Rail (grouped) view via its Auto / Inline / Rail toggle. - GROUP BY — one chip per grouping column, with aggregate-awareness (see below).
- HAVING — the same pill / Rail treatment as WHERE, gated on a GROUP BY already existing (see below).
- ORDER BY — one row per sort entry, each with a column picker, ASC / DESC toggle, NULLS-first / -last toggle (where the dialect supports it), and remove.
- Named windows — one card per named window definition, when the statement has a top-level
WINDOWclause. - LIMIT / TOP / FETCH / OFFSET — one line headed by the statement's own keyword, with a click-to-edit number in a chip (its × removes it):
TOPon SQL Server,LIMITon PostgreSQL / MySQL / SQLite (plusOFFSETwhen present),FETCH FIRSTon Oracle. With no limit yet, the clause row at the end of the SELECT offers it under the keyword it will write (+ Top, + Limit or + Fetch first).

SELECT — joins
JOINs render as strips inside the FROM section, one per join. A single join sits on the same line as its table, the way the SQL reads (orders o then INNER JOIN customers c ON …), and moves below it when the line has no room; two or more joins stack beneath the table. Each strip carries a kind dropdown (INNER, LEFT, RIGHT, FULL, CROSS, CROSS APPLY, OUTER APPLY, LATERAL — the available options change to match the dialect), an inline-edit ON / USING condition, and a remove. Self-joins keep the alias on the right-hand pill so you can tell the partners apart.
SELECT — CTEs
A WITH clause renders each CTE as its own card above the body. Recursive CTEs carry a "recursive" badge in the header and, for the common two-branch shape, label their Anchor and Recursive step parts — the recursion happens inline in the body, and it's fully editable (see below), not just displayed. Each card has a rename and remove affordance and an expand toggle so you can collapse a long CTE you have already understood. Cross-CTE references are resolved by the renderer — clicking a CTE name in the body card highlights the source CTE card.
SELECT — subqueries
Subqueries in FROM and JOIN appear as expandable cards inside the FROM section, headed by the derived table's alias rather than its raw SQL. Expand one and its own mini SELECT/WHERE/GROUP BY editor renders inline — fully editable (add a WHERE, change columns, and so on), not just a read-only preview, including LATERAL derived tables; a focus button still opens it in its own full view when you want more room. A column IN (SELECT …) predicate in WHERE renders as a clean wrapping block with a top-right ✕; click the drill button on the block to open the inner query in a nested view and edit it — this works in both the Visual view and the compact editor popover. Other subquery forms (EXISTS, scalar subqueries) render as collapsible inline panels.
SELECT — set operators
UNION / UNION ALL / INTERSECT / EXCEPT compose multiple SELECTs into a pipeline. VQE renders each branch as its own SELECT card with a labeled bar between them showing the operator. Click the operator badge to open a dropdown and switch the pipeline's kind — UNION, UNION ALL, INTERSECT, EXCEPT (Oracle offers MINUS in place of EXCEPT, matching its dialect) — without retyping the branches; the branches themselves can't be reordered. When the buffer puts a set operator inside parentheses (precedence override), VQE flags the branch with an "in parens" badge so the precedence is visible. A set operator inside a CTE's own body — as in an ordinary recursive CTE — renders as a normal pipeline and isn't affected by this precedence check at all.
SELECT — window functions
Window functions show their OVER (…) body behind an expand toggle. In the Visual view, once the schema has loaded, expanding it opens structured PARTITION BY, ORDER BY and frame pickers that rewrite the whole OVER (…) clause; Edit as text opens the raw clause instead, and an OVER body the parser cannot structure opens as text automatically. The compact popover keeps an inline window's chip locked and shows a named window (OVER w) as a summary.
SELECT — QUALIFY, GROUP BY ALL and ORDER BY ALL
Fabric Data Warehouse and Oracle 26ai add clauses the Visual view shows read-only. Every visual edit keeps them exactly as written (see Fabric Warehouse T-SQL syntax for what the SQL view does with them):
- QUALIFY (SQL Server and Oracle) is its own read-only row between HAVING and ORDER BY, with the line Filters rows by window-function results. Edit it in SQL view. It is never folded into the FROM or WHERE card. A column, alias or table named
qualifyis still read as a name, and on PostgreSQL, MySQL and SQLitequalifyis always a name. - GROUP BY ALL on its own is a read-only
ALLchip: Groups by every SELECT column that isn’t an aggregate. The Visual view does not add columns to it (GROUP BY ALL already groups by every column.), and it never shows the "not in GROUP BY" warning or Fix all. SQL Server's olderGROUP BY ALL a, bkeeps its column chips, adding and removing columns, and the warning; itsALLchip explains that it also returns the groupsWHEREfilters out, and removing the last column removesALLtoo. - ORDER BY ALL (with an optional
ASCorDESC) is a read-only chip: Sorts by every SELECT column, left to right. Adding a sort column is refused. - Where new clauses go. A new WHERE, GROUP BY or HAVING is written before QUALIFY, and a new ORDER BY or row limit after it. Removing or clearing WHERE conditions and the results grid's Add to WHERE leave QUALIFY unchanged. When adding a join qualifies the query's column names with table aliases, the names inside QUALIFY are qualified too; a select-list alias there stays as it is.
- Aggregates.
ANY_VALUE,MEDIAN,QUANTILE,APPROX_MEDIANandAPPROX_QUANTILEcount as aggregates, so a select-list item that calls one is not reported as missing from GROUP BY. On MySQL, whereANY_VALUEis not an aggregate and does not combine rows, a query that uses it without GROUP BY gets no "not in GROUP BY" warning. - Compact editor. The popover shows GROUP BY ALL and ORDER BY ALL as the same read-only chips. For a statement with QUALIFY it shows only the notice Has a QUALIFY clause, which the quick editor doesn’t show — open the full editor. with Open full editor.
FROM-first queries (SQL Server only) are not diagrammed; see Read-only fallback for unsupported shapes. A FROM-first branch of a UNION, EXCEPT or INTERSECT shows its SQL text read-only, while the other branches stay editable.
INSERT
INSERT statements render as a stack of two to four cards depending on shape:
- Target — schema-qualified table name with the column list as a row of pills, editable (see below). Change table beside the table name points the INSERT at another table; the column list and VALUES stay as written.
- Source — one of: a VALUES grid (rows × columns, with click-to-edit cells), an inline SELECT card (the full SELECT renderer, for INSERT…SELECT — with a focus button that drills into the nested SELECT so you can edit it in place), or a DEFAULT VALUES marker.
- ON CONFLICT / ON DUPLICATE KEY — when present, renders as an editable card with the conflict target and the action body (DO NOTHING / DO UPDATE …). PostgreSQL and SQLite use
ON CONFLICT; MySQL / MariaDB useON DUPLICATE KEY UPDATE. - RETURNING — when present, renders as an editable column list. Available on PostgreSQL and SQLite 3.35+. Oracle's
RETURNING…INTOand SQL Server'sOUTPUTaren't modeled as editable tails.
UPDATE
UPDATE statements render as Target → SET assignments → optional FROM / JOIN → WHERE → optional RETURNING. The target is a single amber row — UPDATE, the table, and Change table — rather than a card of its own. Change table opens a searchable table picker and points the statement at the table you pick in one edit: only the name changes, so an alias and everything written through it stay as they are. It is disabled when the target is an alias of a table in the FROM list (SQL Server's UPDATE p … FROM Production.Product p); change that one with Edit as SQL. Each SET assignment is a row column = value: click the column name to pick another column (the value stays), click the value to edit it. A trailing comment (SET StatusCd = 1 -- mark as paid) shows after the value in muted italics: editing the value leaves the comment in place, and a new assignment added after it starts on the next line, so the comment never swallows it. A column the target table doesn't have — after Change table, or a typed name — carries a warning mark with the reason. UPDATE…FROM (PostgreSQL) and UPDATE…JOIN (MySQL / MariaDB) render their join sources as a sub-section under the SET card.
An UPDATE without a WHERE clause changes every row of its table, so the target row carries a red No WHERE · all rows badge (in the compact popover it sits beside the header), and the WHERE row turns red and reads none, so this UPDATE changes every row in orders. Its Add condition writes the first condition, and the badge goes away as soon as there is one. When a JOIN, a joined FROM list or a MySQL LIMIT decides which rows change, the badge reads No WHERE and its tooltip names the join or the LIMIT instead.
DELETE
DELETE statements render as Target → optional USING / JOIN → WHERE → optional RETURNING. The target is a single red Delete from row with the same Change table picker (disabled when the DELETE names its targets before FROM, as in DELETE p FROM …). The USING / JOIN section uses the same JOIN strip layout as SELECT. A DELETE without a WHERE clause gets the same red No WHERE · all rows badge and red WHERE row as an UPDATE, worded for a deletion.
MERGE
MERGE statements render as Target → Source (table or subquery) → ON condition → WHEN branches — target, source and ON are one line each, so the WHEN branches sit near the top — on SQL Server, PostgreSQL 15+, and Oracle (MySQL and SQLite don't have a MERGE statement). The target row has Change table too. Each WHEN branch is its own card showing the match kind (WHEN MATCHED, WHEN NOT MATCHED, WHEN NOT MATCHED BY SOURCE) and the action body (UPDATE assignments, INSERT column-and-values, or DELETE). MSSQL's BY TARGET / BY SOURCE qualifiers are surfaced; standard / PostgreSQL MERGE branches without a qualifier render as plain WHEN MATCHED / WHEN NOT MATCHED. Branches are editable — you can add a branch, switch a matched branch's action between UPDATE and DELETE, and remove a branch (see below).
Read-only fallback for unsupported shapes
VQE renders a safe read-only fallback view for buffers it cannot diagram structurally:
- DDL (
CREATE,ALTER,DROP,TRUNCATE, …) — one card with the statement kind and object (for example CREATE TABLE audit_log), the parsed facts (such as the column count), one line — "The Visual Query Editor does not edit DDL. Open the statement in the SQL editor, or open the table in Design table." — a Show statement text disclosure, and the buttons: Design table (for CREATE / ALTER TABLE, the solid one) and Edit as SQL. - Procedural bodies (
BEGIN…END,CREATE PROCEDURE,CREATE FUNCTION, anonymous PL/SQL blocks) — renders the routine name and a hint that the body is read-only in VQE. - FROM-first queries (SQL Server;
FROM t SELECT …, also after aWITHlist or as the source of anINSERT) — a card titled FROM-first query — read-only that says FROM-first queries can be edited in SQL view. In the SQL view the cursor statement shows a FROM-first query label where the pencil would be. A(FROM …)subquery inside an ordinary SELECT, and a FROM-first branch afterUNION,EXCEPTorINTERSECT, is shown read-only. - Set-operator precedence buffers — when paren-grouped set operators make a single canonical render impossible, VQE shows the outer pipeline only and falls back for the inner branches.
- Parse errors — when the buffer cannot be parsed at all, VQE shows the error position (line + column), the unexpected token, and an Edit-as-SQL button that lands the cursor at the error offset.
- No statement at cursor — when the buffer is comments-only or whitespace, VQE shows a neutral "no statement at cursor" panel.
Every fallback view ends with an Edit as SQL button that switches the tab back to its SQL view at the right offset. The fallback is not a dead end — it is a controlled landing zone with the escape hatch always one click away. Because the fallback panel already explains why the statement is read-only and owns that button, no readiness banner is shown above it — one message and one primary action per screen.

Edit as SQL — the escape hatch
Every structure card has an Edit as SQL action. In the Visual view it switches the same tab back to its SQL view with the statement you were looking at selected, so any keystroke replaces it. In the compact editor popover it closes the popover and selects the statement in the editor underneath. Nothing is copied or written back: there is one document, and your visual edits are already in it.
Parse-error fallbacks land the cursor on the error offset instead of selecting a range, so you can fix the typo where the parser flagged it.
Editing — round-trip from structure to SQL
Every clause card carries the inline pills and Add buttons that drive structural edits back into the buffer. Edits route through the same SQL parser the IntelliSense uses, so the buffer round-trips byte-for-byte: nothing is reformatted, no comments are dropped, identifier quoting stays intact. Every edit is undoable with Cmd/Ctrl+Z — in the Visual view and in the compact popover, each edit is a single undo step on the Query Editor's own history. After each edit, a short Edit applied status with an Undo button appears where you made it — in the Visual view's header, or in the compact popover's footer next to Done — and clears itself after a few seconds or as soon as the SQL changes again.
While the Edit applied status shows, the pills and rows the edit touched are tinted — a new column, a changed filter, a new join row — so you can see what the edit changed. Running the statement with Execute clears the status and the tint.
An edit affordance is enabled when two things are true:
- The tab is bound to a database connection (so the renderer knows what tables and columns are reachable). Tabs opened without a connection show the disabled scaffolded state — the structure still renders for reading.
- The chooser needs schema metadata to populate (column / table / join suggestions). Free-text affordances like add CTE and add VALUES row activate as soon as the connection is bound; suggestion-driven affordances like add join wait for the schema snapshot to load. The UPDATE / INSERT column pickers open right away: before the snapshot loads they take a typed column name, and the table's columns appear once it has.


SELECT list — add and remove columns
Click Add column in the SELECT list card to open a searchable chooser. By default it lists only the columns from the tables actually in your query's FROM / JOIN — not every table in the schema — grouped under each source table or alias and annotated with their data type and a PK / FK key hint, so you pick only the columns that make sense. Type to filter across every group. On PostgreSQL and SQL Server, two tables with the same name in different schemas stay separate groups, titled with their schema (for example orders (sales)). An unqualified table name resolves the way the server resolves it — along the search_path on PostgreSQL, in your login's default schema and then dbo on SQL Server — so a table that exists only in another schema reads as not in the schema cache until you qualify it (with an empty search_path, every unqualified name does). A CTE, derived table or table function has no columns Jam can list: the chooser says so (Not listed: the columns of r (CTE), or No columns to list when the query reads nothing else) instead of offering unrelated tables, and a schema table that shares a CTE's name is never offered in its place. A Show all tables toggle at the bottom widens the list to the whole schema when you need it, and each table group has a + all action that adds every column from that table in one click. Columns already in the SELECT list carry a check mark, and picking a checked column removes it again; once some of a table's columns are in, its group action reads + N remaining and adds only the missing ones (it disappears when every column is in). Pick an unchecked column and a new SELECT item appends with the right alias quoting for your dialect. If a SELECT item references a table that isn't in the FROM/JOIN, it's flagged in red with an unknown table badge (its tooltip says the table is not in FROM/JOIN) so you can add the table or remove the column. The per-row × button removes a column. Both edits preserve the SELECT list's existing order and whitespace; the only thing that changes is the items themselves.
After Add column sits a DISTINCT toggle chip — click it to add or remove SELECT DISTINCT from the statement; it is filled while DISTINCT is on. It's available in both the Visual view and the compact popover, editing the same shared buffer either way.
FROM — add joins and tables
The FROM card's action line leads with Add join (join a related table — see below), offers Add cross-join as the second affordance, and — while the statement has no WITH clause — Add CTE as the third; the line sits at the end of the last join row when it fits. A join row starts at its join type (INNER JOIN ▾); the table → prefix naming the table it attaches to appears only on a later join, where more than one table is available. Add cross-join opens a table chooser drawn from the connection's schema and appends a comma-separated FROM item with the dialect-correct schema-qualified syntax — that produces a Cartesian product, which is why it is labelled a cross-join rather than a plain table add. You can also drag a table or view directly from the Object Explorer into the FROM card to add it as a FROM source. The per-row × button drops a FROM item, including any payload (CTE refs, subqueries, derived tables); JOINs that depend on the dropped item are not auto-removed — VQE flags them inline if the parser detects an unresolved reference. When a button is disabled, hover it for a one-line reason and the suggested alternative.
WHERE and HAVING — add, modify, remove filters
WHERE and HAVING share the same filter engine — everything below applies to both clauses; the HAVING-specific differences are called out at the end.
The WHERE (or HAVING) card offers a single Add condition button to append a new condition. It opens a searchable column picker with the columns grouped by their source table. It lists only the tables the statement reads, so unlike the SELECT pickers it has no Show all tables toggle. Once the clause already holds a predicate, an AND / OR connector toggle at the top of the picker chooses how the new condition joins. When the new condition's connector differs from the clause's own, the existing clause is wrapped in parentheses first, so it keeps its meaning: adding c with AND to a OR b writes (a OR b) AND c, and adding with OR to a AND b writes (a AND b) OR c. This applies to WHERE, HAVING, and the WHERE of an UPDATE or DELETE. A new WHERE clause goes before GROUP BY, ORDER BY, LIMIT / FETCH, WINDOW, and a trailing FOR UPDATE / FOR SHARE, MySQL's LOCK IN SHARE MODE, or SQL Server's FOR XML / FOR JSON / OPTION (…).
When the clause is shown inline, each condition carries a small + or chip (+ and when the conditions are joined by OR) that groups it with a new condition by the other connector — for example (status = 'shipped' OR city = 'London') AND total > 40. Grouping by the same connector would change nothing, so the chip's picker goes straight to the columns. Each parenthesised group ends with a chip for its own connector that adds one more condition into the group. The chips show only on the condition under the pointer or with keyboard focus (and on touch screens always), so a WHERE carries no chips at rest; a chip stays visible while its picker is open, and its condition editor opens in the row.
Once the WHERE or HAVING clause holds two or more conditions, a Clear all button next to Add condition removes the whole clause in one step, and Ctrl/Cmd+Z brings it back. On an UPDATE or DELETE it first asks for confirmation, the same way removing the last condition does, because the statement would then change every row.
Once you pick a column, a full filter editor opens (the value box says Text, Number, or Value by the column's type, and the apply tick stays grey until there is a value, then turns into a filled button — Enter applies too) with the same capabilities as Table Explorer filters: enum value pickers (for native enum columns, check-constraint enums, and sampled categorical values), JSON-path sub-operators (for JSON/JSONB columns), loose-FK lookup buttons (for columns that carry a logical foreign-key reference in your database metadata), and the in list / not in list operators on text, number, date and uuid columns (see below).
Inside a drilled subquery the column picker also lists the enclosing levels' tables that the engine lets the subquery reference: the tables of the CTE body or UNION branch it sits in, the levels outside a derived table (never the derived table's siblings), a surrounding UPDATE or DELETE's joined tables, and, for a LATERAL / APPLY body, only the FROM items before it (on SQL Server only those in its own comma-separated source). A subquery inside a CTE body that itself sits in a subquery also sees the levels outside the WITH on PostgreSQL, MySQL and SQLite (Oracle rejects that reference, so there it stops at the CTE body). A CTE name reaches only inward, so a WITH inside a subquery never hides an outer table of the same name. A MERGE's USING subquery never sees the target. An inner alias the subquery can see hides an outer table of the same name by the engine's identifier rules; a FROM item after a LATERAL / APPLY body does not.
The editor uses your database's MetaInfo exactly as Table Explorer does, for a table in any schema and through an alias or a join: a column with a loose foreign key defaults to lookup, and the row picker applies the relationship's target filter (for example, it hides soft-deleted rows); PostgreSQL CREATE TYPE … AS ENUM columns get the enum picker without a declaration; in HAVING, MIN / MAX take their column's type, so MAX(created_at) gets a date input. Date and timestamp values are sent as you picked them, except where the engine needs another form: Oracle gets TO_DATE / TO_TIMESTAMP (so they work under any NLS_DATE_FORMAT) and SQL Server gets the seconds it requires. Lookup and enum values on a date column follow the same rule. When a column has a real single-column foreign key, the lookup follows it and a loose declaration on the same column never adds its target filter; a column that is only part of a composite foreign key uses the loose declaration made on it. The spatial operator is not offered in the Visual Query Editor — use Table Explorer's map filter for that. MetaInfo saved while the tab is open — a loose FK, an enum or JSON declaration from Table Explorer, the MetaInfo manager or an AI agent — applies without reopening it, and an open editor switches to the JSON operator if you haven't changed it yet.
On PostgreSQL the editor reads each column's type as Table Explorer spells it (integer[], character varying(10)), so it offers the same operators. An array column gets the array operators — contains, does not contain, is empty / is not empty, the length comparisons, and lookup — and a lookup on an array column writes 42 = ANY(tag_ids). A short varchar(n) or char(n) column offers the enum operator for its sampled values.
A pill whose column references a table that isn't in the FROM/JOIN is shown in red with an unknown table badge. The × button on a filter pill removes it; removing the last filter drops the clause from the buffer. A column IN (SELECT …) predicate renders as a clean wrapping block with a top-right ✕ and a drill button — clicking it opens the subquery as a nested query in its own view, in both the Visual view and the compact editor popover.
in list and not in list
Text, number, date and uuid columns offer in list (IN (…)) and not in list (NOT IN (…)), as Table Explorer does. Type a value and press Enter to add it to the list, or paste several at once — one per line, or separated by tabs or commas. Enter on an empty entry applies the condition, and Backspace on an empty entry removes the last value. Each value is written the way = writes it for that column, so numbers stay unquoted and dates take the engine's date literal. Like not equals, not in list never matches a NULL cell. Oracle accepts at most 1,000 values in one list, so there a longer list is written as 1,000-value lists inside one pair of parentheses (joined by OR for in list, AND for not in list), which returns the same rows.
Reopening a filter
Clicking an existing filter pill opens the same editor pre-populated with the current condition, so you can change the operator or value without retyping the whole predicate; on a loose-FK lookup it opens the declared relationship. Every condition the editor writes reopens as that editor: BETWEEN with both bounds, the true / false switch on a boolean column, JSON filters on array elements (any / all, including enum and lookup picks), $.. paths, is empty / is not empty, SQL Server's has property, PostgreSQL 19's case-insensitive JSON matches, and the PostgreSQL array operators. A single value picked from an enum reopens as the enum picker, and = on a column with a real foreign key reopens as the lookup. A typed IN (…) or NOT IN (…) list reopens as in list / not in list, or as the enum picker on a declared, native or lookup enum column. A single condition in parentheses, such as (a = 1), reopens in the editor too.
Reopening never changes a condition: pressing Apply without changing anything keeps the condition exactly as written (for example a plain 'abc' against a SQL Server nvarchar column), and only a condition you edit is rewritten the way the editor writes it. One the editor can't hold exactly stays editable as SQL text instead: an integer beyond 253, a bound written as an expression such as 'a' || 'b', a MySQL string with backslash escapes, or a spelling the editor doesn't write. Other predicates that cannot be parsed into the structured editor (raw SQL expressions, functions) also open as an inline text input.
One filter, one pill
A few filters are written as more than one SQL predicate: JSON property != ((x IS NULL OR x <> 'v')), JSON is empty / is not empty on every engine but MySQL, and a JSON filter on a text column declared as JSON, which puts a validity guard before the test. The editor writes such a filter in one pair of parentheses, and it shows as one pill in the Inline view, one leaf in the Rail and one pill in an UPDATE or DELETE WHERE. Editing it keeps the guard. A guard + test pair written earlier without parentheses also shows as one pill, and editing or removing it acts on both parts. When you edit a condition as text and the new text has its own top-level AND or OR, it is written in parentheses, so it stays one condition next to its neighbours.
Conditions that end in a comment
A condition can end in a line comment (-- note, or # note on MySQL). New conditions, the closing parenthesis of a wrapped clause, and the SQL after an edited condition then start on the next line, so the comment never swallows them and the statement stays valid.
Value labels on pills
A pill whose value is a lookup key (on a column, or on a JSON path declared as a loose foreign key) or a lookup-enum key shows the label Table Explorer's filter chip shows, after the condition as written: o.customer_id = 42 — Acme Corp, o.status_id IN ('3') — Active. The labels come from the same cached reads as the chip — the referenced row for a lookup, MetaInfo's key / label pairs for a lookup enum — and nothing is added while a label loads or when none resolves. They appear on the Inline pills, the Rail leaves, an UPDATE or DELETE WHERE and the compact popover.
JSON columns: sample and peek
The condition editor's Declare as JSON dialog shows a sample of the column: the tab's last results when they carry it (SQLite reports no source columns for results, so there they never do), otherwise up to 20 non-NULL values that match the query's other conditions, read while the dialog is open. The JSON path peek scans only the rows the query reads: it applies the statement's other AND-joined conditions on the same table and alias that the editor can reopen, and leaves out OR groups, NOT, other tables and raw text. Scan whole column instead switches the peek to the whole column.
HAVING adds two things on top of that. Its Add picker leads with an Aggregates group — COUNT(*), COUNT, SUM, AVG, MIN, and MAX templates — so you don't have to hand-type an aggregate expression; picking a zero-argument template like COUNT(*) commits immediately, and picking SUM / AVG / MIN / MAX re-opens the column list for the argument. And HAVING's empty Add affordance only appears once the statement already has a GROUP BY — a bare SELECT … FROM t has nowhere to invite a HAVING clause, so the section stays hidden until you add one. (A HAVING clause that's already in the buffer, without a GROUP BY, still renders and edits normally — the gate only blocks the empty-state invite.)
One asymmetry to know: adding a HAVING condition on an aggregate goes through the structured Aggregates picker above, but editing an existing aggregate pill — clicking COUNT(*) >= 2 to change it to >= 3 — falls back to a plain text input instead of the structured chip. The shared round-trip parser behind click-to-edit only recognizes plain column operator value shapes; any field containing a function call (an aggregate, or something like LOWER(name)) can't be represented structurally, so the pill opens for free-text editing instead. Press Enter to commit, Escape to cancel, same as any other filter.
JOINs — add, change kind, edit ON, remove
Inside the FROM card, each JOIN strip exposes:
- Add join — opens a searchable chooser that lists foreign-key-linked tables under a Linked by foreign key heading (ranked first; FK detection works across all engines, including PostgreSQL and Oracle; pick one and Jam emits a fully-resolved
INNER JOIN ... ON ...with the FK columns pre-filled. The joined table gets an alias, so a table already in the query that has none gets one in the same edit, and every column of the query is qualified with it:SELECT id, name FROM customersbecomesSELECT c.id, c.name FROM customers c INNER JOIN orders o ON o.customer_id = c.id. If the table's name is also used somewhere Jam does not rewrite — inside a subquery, or asschema.table.column— it keeps the name and qualifies the columns with it instead) and every other reachable table under Other tables, with the tables already in your query last under Self-join; type to filter every group. Relationships declared as loose foreign keys in MetaInfo are listed separately under Linked by loose relationship with a Loose FK badge; their source and target filters become part of the generatedON(also for a LEFT JOIN and in either direction), so a polymorphic reference joins only the rows of its type. Table and column names are written as the query spells them, quoted where the dialect needs it ("order items"."qty",[Order Details].[Qty], a mixed-case PostgreSQL table aspublic."OrderLines"), and a joined table is schema-qualified (public.film,dbo.Orders). Clicking an existing join's ON pill opens the table-and-column editor for quoted and bracketed names too; it lists only the tables before that join, and a side you do not change keeps what you typed. A self-join opens the same ON-column step with the table's existing alias on the left and a new alias (for examplec2) on the right, soc.manager_id = c2.idis two clicks. Picking a table with no foreign key opens a structured ON-column step: you pick the table and column on each side of the join condition — the left side can be any table already in the query, not just the first FROM table — with no same-name column auto-selected. Refine the predicate afterward via the inline ON pill. - Kind dropdown — flip INNER ↔ LEFT ↔ RIGHT ↔ FULL ↔ CROSS. Switching to a CROSS-style kind drops the ON predicate; switching back synthesizes a placeholder predicate that you edit inline.
- ON pill — click to edit the ON expression. A simple
a.x = b.ypredicate opens a structured two-column editor (a table picker and a column picker on each side of the=), with an Edit as text escape for anything more complex; everything else opens free-text editing. The table pickers list only the tables that ON can see: an earlier comma-separated FROM item is left out (SQLite allows it, so there it is offered), a table after the JOIN is never offered, and an ON that names one opens as text. The list is built when the editor opens, so a long join chain stays fast. Tables whose names differ only by case, such as"A"anda, stay separate wherever the engine keeps them apart (PostgreSQL, Oracle, SQL Server with a case-sensitive collation, MySQL withlower_case_table_names=0). Enter commits, Escape reverts. Multi-column equality predicates (a.x = b.x AND a.y = b.y) are preserved verbatim on commit. - × button — removes the join, including its ON predicate.
ORDER BY — add, toggle, remove
Sort by column opens the same searchable, table-grouped chooser as Add column — it defaults to the columns from the tables in your query (with a Show all tables toggle and a per-table + all bulk action). Columns that are already sort keys are checked; picking one removes it. Each ORDER BY entry's direction toggle flips ASC ↔ DESC in place; the × button removes a sort entry, and an entry that references a table not in the FROM/JOIN is flagged in red with an unknown table badge. NULLS FIRST / LAST is rendered as a per-row toggle on dialects that support it (PostgreSQL, Oracle, SQLite 3.30+).
LIMIT / TOP / FETCH / OFFSET — inline numeric edit
Each value sits in a chip like the other clause items. Click the number to edit it in place — the input only accepts digits; Enter or clicking away commits the new value, and Escape reverts to what was there before. The chip's × removes that value; removing the last one drops the clause. Dialects with a separate OFFSET (PostgreSQL, MySQL, SQLite) get their own chip alongside the row count, so you can change or remove one without disturbing the other. Oracle's FETCH FIRST … ROWS ONLY shows as FETCH FIRST n rows — only the number is what you edit, and the SQL keeps its full keyword text.
GROUP BY — add, reorder, remove (with aggregate awareness)
If the SELECT list contains aggregate functions (COUNT, SUM, AVG, MIN, MAX, plus dialect-specific extensions) and some of its other columns aren't in GROUP BY yet, the SELECT list card — not the GROUP BY card — surfaces a small warning ("N columns aren't in GROUP BY") with a one-click Fix all button that adds every missing non-aggregate column to GROUP BY at once; each affected SELECT pill also carries its own inline warning icon for a one-column fix. The Group by column chooser on the GROUP BY row (or + Group by in the clause row at the end while the clause is empty) behaves like ORDER BY's — searchable, grouped by the tables in your query, with a Show all tables toggle, a per-table + all bulk action, check marks on the columns already grouped on (picking one removes it), and a red unknown table badge on out-of-scope entries. Drag a chip's handle to reorder the GROUP BY list — this works in both the Visual view and the compact popover. The × button removes an entry; removing the last one drops the GROUP BY clause.
CTEs — add, rename, remove
The CTE section above the body card supports:
- Add CTE — on the FROM card's action line while the statement has no
WITHclause, then on the WITH card itself. Opens a small two-input form (name + body SQL). Submitting prepends the CTE to the WITH list, or synthesizes a newWITH name AS (body)wrapper if the buffer didn't have a WITH clause. - Inline rename — click the CTE name pill, type a new name, press Enter. Jam case-folds and renames every reference to the CTE — including the CTE's own self-reference inside a recursive definition's recursive member — across the rest of the statement (subsequent CTE bodies + main statement body), excluding dotted references like
t.cte_name. - Remove — drops the CTE definition and its trailing comma, with a confirmation prompt. Recursive CTEs support the same rename, remove, and body editing as non-recursive ones — the "recursive" badge and Anchor / Recursive step labels are informational, not restrictions.
Each CTE card also has an expand toggle, and — for a recursive CTE whose body is a plain anchor-member UNION (or UNION ALL) recursive-member shape — the two branches render as their own labeled Anchor / Recursive step cards, each independently editable. A focus button lets you drill into the CTE body as a nested view, the same way an INSERT…SELECT source does.
INSERT / UPDATE / DELETE / MERGE — DML edits
The DML cards expose the same shape as SELECT:
- INSERT columns — Add column opens a column picker over the target table's columns: listed ones are check-marked, and typing a name the table doesn't list offers Use name (the button is disabled once every column is already listed). Click a column pill to swap it for another column — the VALUES cells stay in place; × removes a column, though the last one can't be removed.
- INSERT VALUES — each cell is click-to-edit and suggests
NULL/DEFAULTand the engine's functions, typed by the cell's column (no column names: VALUES can't reference the table's columns); Add row opens a column-aware row form (one input per known column); the per-row × removes a row. - INSERT…SELECT — the source query's focus button drills into the nested SELECT as its own full renderer, with a breadcrumb back to the INSERT card.
- INSERT tails — ON CONFLICT (PostgreSQL / SQLite) and ON DUPLICATE KEY (MySQL / MariaDB) render as editable cards with a conflict-target / action switch; RETURNING (PostgreSQL / SQLite 3.35+) renders as an editable column list. See the INSERT section above for the full per-engine breakdown, including SQL Server and Oracle's non-goals.
- UPDATE assignments — Add assignment opens a column picker listing the target table's columns with their types (columns already in SET are check-marked and can't be picked twice, however the name is spelled; typing a name the table doesn't list offers Use name). Picking a column adds a draft row
column = [value]at the end of the list. The value input suggests as you type — the table's columns,NULL/DEFAULT(andTRUE/FALSEfor a boolean) where the engine accepts them, and the engine's built-in functions, those matching the column's type first; on a fresh draft the list is open before you type, withNULLfor a nullable column andEXCLUDED.columnin an ON CONFLICT DO UPDATE at the top. ↑ / ↓ move, Enter or Tab take a suggestion (a function lands with the caret inside its parentheses), Ctrl+Space lists everything, and Esc closes the list, then cancels the draft. With the list closed, Enter or ✓ applies. A text column's placeholder shows the quoting ('text'). New columns follow the list's own style, so a MERGE branch that writest.qtygetst.note; in a MySQLUPDATE a JOIN b … SET b.x = …the joined table's columns aren't known, so its rows take typed names. Clicking a value edits it with the same suggestions, and the same picker swaps the column of an existing row. The ON CONFLICT / ON DUPLICATE KEY and MERGE SET lists work the same way. × removes an assignment. - DELETE WHERE — reuses the WHERE filter affordances from SELECT. Identical commit semantics.
- MERGE branches — an add-branch button at the end of the stack opens a new
WHEN MATCHED,WHEN NOT MATCHED, orWHEN NOT MATCHED BY SOURCEbranch (matched→UPDATE, matched→DELETE, or not-matched→INSERT); each matched branch also carries an action-kind dropdown to switch UPDATE ⇄ DELETE. Nested SET assignments inside a matched UPDATE branch support the same add / modify / remove operations as a top-level UPDATE. MSSQL and PostgreSQL 15+ allow multiple same-kind branches, disambiguated by anANDcondition; Oracle allows at most one matched and one not-matched branch (the add-branch button disables once both exist) and has no standalone DELETE branch, so its action-kind switch stays disabled. × removes a branch.
Edit-as-SQL — still the universal escape hatch
For shapes structural editing does not cover (non-standard window frames, MERGE's source / ON predicate, dialect-specific syntax the parser flagged as a recovery point), every card still carries an Edit as SQL button that switches to the SQL view with the cursor at the right offset. Use it when the structural edit you want isn't expressible — anything you can type into Monaco is fair game.
What does not ship
- Set operator branch reorder. UNION / INTERSECT / EXCEPT branches render as labeled cards and their operator kind is editable, but the branches themselves can't be reordered — remove and re-add via Edit-as-SQL if you need a different order.
- Per-pill negation toggle. Individual filter pills carry no negate control — apply
NOTto a whole AND / OR group from the Rail (grouped) view of the WHERE / HAVING card, or edit the pill text inline. - MERGE source and ON predicate. Branch add/remove, the action-kind switch and the target table (Change table) are structural; the source and ON condition are read-only here — edit them via Edit-as-SQL.
- RETURNING editing is INSERT-only. UPDATE and DELETE display an existing RETURNING list but don't offer an editable tail card for it.
For everything else: edit in VQE, watch the SQL buffer rewrite itself, and Cmd/Ctrl+Z when you change your mind.
Run your query
The Visual view doesn't just edit SQL — it runs the statement it shows without leaving the tab. The Query Editor toolbar's Execute button reads Statement (F5 and Cmd/Ctrl+Enter over the cards do the same) and runs the statement selected in the statement picker; Stop works as in the SQL view. Destructive statements (DELETE, DROP, TRUNCATE, and friends) go through the same confirmation dialog as in the SQL view — nothing runs unconfirmed just because you're in VQE.
For an UPDATE or DELETE, the header also has an Estimate rows button. Clicking it runs a COUNT with the statement's own filter (nothing is changed) and shows the result in its place, for example ≈ 42 rows; it turns amber for an UPDATE and red for a DELETE once there is a number. A multi-table form shows an upper bound instead (~42 matching rows).
Results land in the tab's own results pane — the same grid, copy / export menu, Results and Messages tabs, divider, and status line as in the SQL view. If you edit the statement after running it, a small notice tells you the results are from an earlier version of the query, until you run it again — editing alone never clears the notice, only a fresh run does. Running works from the starter canvas too: pick a table, and the seeded SELECT * FROM <table> is a real, executable query from the moment it lands.
For a SELECT, the Visual view's header has a Run on change switch. While it is on, the statement re-runs after each visual edit, so the results grid follows the query as you add filters, sorts and columns. It never runs an INSERT, UPDATE, DELETE, MERGE or DDL statement, and the switch is not shown for them. It is the same setting as the compact popover's switch and Settings → UI → Visual editor, so turning it off in one place turns it off in all three.
From the results grid: Add to WHERE and Add to Filter
The results grid under the Visual view has the same two filter entry points as the SQL view's (see the Query Editor), and both open the WHERE condition editor instead of editing text:
- Add to WHERE (right-click a cell) opens the condition editor pre-filled with the condition Table Explorer's Add to filter starts from:
IS NULLfor an empty cell, contains for an array cell, lookup on a foreign-key column,=otherwise. The column is found in the statement the way the statement names it — the alias in a join, the name a derived table or CTE exposes — so a lookup, enum or JSON column gets its own editor, and applying writes the same SQL as the filter chip. - Add to Filter (first item on a column header's right-click menu, as in Table Explorer) opens the condition editor for that column with its default operator and no value. It is disabled, with the reason in its tooltip, for an aggregate, window or expression column, for a column of a derived table, CTE or table function, for UNION / INTERSECT / EXCEPT results, and for results that don't come from a SELECT the Visual Query Editor can edit. It isn't shown while the Visual Query Editor is turned off.
Both leave a drilled subquery first, since the results belong to the whole statement. When the column can't be placed in the statement, or the view can't add a condition to it, a Not added to WHERE message says why.
Performance & budget
The Visual Query Editor is designed to feel instant on real-world queries:
- Render latency — every supported shape renders in under 50ms on representative buffers (10-line SELECT, 40-line JOIN-heavy SELECT, recursive CTE). The render cost is amortized across the parser and the structural extractor; only the statement under the cursor is rendered, not the whole buffer.
- Bundle size — the VQE bundle is <120 KB gzipped. The renderer is lazy-loaded; opening it does not affect the cold-start cost of the rest of the app.
- Re-render rate — buffer changes cause exactly one structural re-render. Unrelated UI mutations (focus changes, expansion toggles) do not invalidate the parsed structure cache.
Reporting parser issues
If VQE renders a buffer in an unexpected shape — wrong fallback, missing JOIN strip, garbled set-operator pipeline — some fallback screens surface a Report button: parse-error fallbacks always get one, and so do the two "this shouldn't happen" read-only fallbacks — an unsupported set-operator precedence shape, or a statement VQE simply doesn't recognize. The two expected fallbacks, DDL and procedural bodies, don't get a Report button, since landing there is by design, not a bug. Clicking Report opens a confirmation dialog showing the exact buffer text and a corpus file path; nothing is uploaded automatically. After you confirm, the SQL is written to a local corpus file you can hand to the Jam team. See the AI Workspace knowledge pack and the developer docs for details on the harness.
Keyboard shortcuts
| Action | macOS | Windows / Linux |
|---|---|---|
| Toggle the Visual view | Cmd+Shift+V | Ctrl+Shift+V |
| Open keyboard-shortcut help | ? | ? |
| Move focus to the next / previous control | Tab / Shift+Tab | Tab / Shift+Tab |
| Activate the focused control, or open a list / statement picker | Enter | Enter |
| Apply the focused filter or value edit | Enter | Enter |
| Cancel the open editor / close the popover or picker | Esc | Esc |
| Pick up or drop a SELECT or GROUP BY chip to reorder it | Space / Enter | Space / Enter |
| Move the picked-up chip to a new position | Arrow keys | Arrow keys |
| Cancel the reorder and keep the original order | Esc | Esc |
| Undo / redo the last applied edit in the SQL buffer | Cmd+Z / Cmd+Shift+Z | Ctrl+Z / Ctrl+Shift+Z |
| Move between statements in the statement picker | Arrow keys | Arrow keys |
| Execute the active statement | Cmd+Enter | Ctrl+Enter |
Press ? inside the Visual Query Editor to open the in-app keyboard-shortcut help dialog, which lists the same shortcuts grouped by section. See the full keyboard shortcuts reference for global navigation, query execution, and results-grid shortcuts.
Engine support matrix
The structural renderer covers every supported engine, and most editing affordances are dialect-aware too — a ✅ below means both rendering and editing work where VQE offers editing for that shape at all; the sections above spell out exactly which parts of each shape are editable. Some shapes are dialect-specific; the renderer adapts the labels and available join kinds per engine.
| Capability | SQL Server | PostgreSQL | MySQL / MariaDB | Oracle | SQLite |
|---|---|---|---|---|---|
| SELECT — basic / JOIN / subquery | ✅ | ✅ | ✅ | ✅ | ✅ |
| SELECT — CROSS / OUTER APPLY | ✅ | — | — | ✅ | — |
| SELECT — LATERAL | — | ✅ | — | ✅ | — |
| CTE (non-recursive / recursive) | ✅ | ✅ | ✅ (8.0+) | ✅ | ✅ |
| UNION / INTERSECT / EXCEPT | ✅ | ✅ | ✅ | ✅ | ✅ |
| Window functions | ✅ | ✅ | ✅ (8.0+) | ✅ | ✅ (3.25+) |
| TOP / LIMIT / FETCH FIRST | TOP | LIMIT | LIMIT | FETCH | LIMIT |
| ORDER BY NULLS FIRST / LAST | — | ✅ | — | ✅ | ✅ (3.30+) |
| INSERT — VALUES / SELECT | ✅ | ✅ | ✅ | ✅ | ✅ |
| INSERT — ON CONFLICT / ON DUPLICATE KEY | — | ✅ | ✅ | — | ✅ |
| RETURNING (INSERT / UPDATE / DELETE) | — | ✅ | — | — | ✅ (3.35+) |
| UPDATE — FROM / JOIN | ✅ | ✅ | ✅ | — | — |
| DELETE — USING / JOIN | — | ✅ | ✅ | — | — |
| MERGE | ✅ | ✅ (15+) | — | ✅ | — |
| HAVING — full editing (pills, Rail, aggregate templates) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Set-operator kind switch (UNION / INTERSECT / EXCEPT / MINUS) | ✅ | ✅ | ✅ | ✅ | ✅ |
| MERGE — branch add/remove + action switch | ✅ | ✅ (15+) | — | ✅ (constrained) | — |
| Recursive CTE — rename / remove / body editing | ✅ | ✅ | ✅ | ✅ | ✅ |
| QUALIFY — shown read-only, kept by every edit | ✅ | — | — | ✅ | — |
| FROM-first query — read-only card | ✅ | — | — | — | — |
Oracle's MERGE editing is constrained by its own dialect: at most one WHEN MATCHED and one WHEN NOT MATCHED branch (the add-branch button disables once both exist), and no action-kind switch, since Oracle has no standalone DELETE branch. SQL Server and PostgreSQL 15+ allow multiple same-kind branches and the full UPDATE ⇄ DELETE switch.
Limits
VQE has a few deliberate limits. They are recorded here so you can plan around them.
- One statement at a time. Multi-statement buffers render the statement under the cursor; switch via the statement picker. There's no side-by-side multi-statement view.
- Edits apply directly. Every Visual Query Editor edit, in the Visual view and in the compact popover, is written straight to the SQL, with no SQL preview step. A few removals ask first: the query's only table, a CTE that is still referenced, a subquery's correlation condition, and the last WHERE condition of an UPDATE or DELETE. Press Cmd/Ctrl+Z to undo an edit. (DDL edits reached through the Table Designer still show a preview first.)
Frequently asked questions
What is the Visual Query Editor in Jam SQL Studio?
The Visual Query Editor (VQE) renders the SQL statement under your cursor as a structural diagram — a stack of clause cards laid out in the order the engine evaluates them: CTEs, FROM, JOINs, SELECT list, WHERE, GROUP BY, HAVING, ORDER BY, and LIMIT. You can both read the structure of a statement and edit it round-trip: add columns, filters, joins, ORDER BY, GROUP BY, HAVING, CTE definitions (including recursive ones), and DML rows — and run the statement without leaving the tab. Every edit writes back to the Monaco buffer, which remains the source of truth.
Which statement shapes does the Visual Query Editor support?
SELECT (with JOINs, CTEs, subqueries, UNION / INTERSECT / EXCEPT, and window functions), INSERT (VALUES, INSERT…SELECT, RETURNING / ON CONFLICT / ON DUPLICATE KEY where the dialect supports them), UPDATE (plain, UPDATE…FROM, UPDATE…JOIN), DELETE (plain, DELETE USING / JOIN), and MERGE. DDL, procedural bodies, and parse-error buffers fall back to a safe read-only view with an Edit-as-SQL escape.
How do I open the Visual Query Editor?
Switch the Query Editor toolbar's SQL | Visual control to Visual: the same tab shows the statement under your cursor as clause cards. Cmd+Shift+V (macOS) or Ctrl+Shift+V (Windows / Linux) and Toggle Visual view in the Command Palette switch between the two views; More → Visual Query Editor on the main toolbar shows the Visual view of the active Query Editor, or opens a new query tab in the Visual view. The gutter pencil next to the statement under your cursor (or an Edit button on the run bar when the Inline Execute/Peek bar setting is on) opens the compact Visual Query Editor popover, whose Open full editor button switches the same tab to its Visual view on that statement.
Can I edit a query from the Visual Query Editor?
Yes. The Visual Query Editor supports round-trip structural editing. Add buttons on each clause card — Add column, Add filter, Add join, Add cross-join, Sort by column, Group by column, Add CTE, and more — open a chooser; pick a value and the edit is written back to the SQL buffer. Inline pills let you rename CTEs, edit filter expressions, change JOIN ON predicates, toggle ASC/DESC, and modify SET assignments. Every edit is byte-precise and undoable with Cmd/Ctrl+Z, and routes through the same parser the IntelliSense uses — so the buffer is always the source of truth.
Does the Visual Query Editor work offline / without a database?
Yes. The renderer is purely structural — it parses the SQL text and reads the resulting outline. No connection round-trip is needed to render a statement, so you can open VQE on any tab, including disconnected ones.
Related docs
- Query Editor — write and execute SQL with IntelliSense, peek results, and column profiling.
- Keyboard shortcuts — full reference for global, editor, and results shortcuts.
- Scripting — generate DDL scripts from the Object Explorer.
- Changelog — every release of Jam SQL Studio.