# SQL editor

> DuckLocal's DuckDB SQL editor: query tabs, formatting, inline errors, schema-aware autocompletion, Explain plans, per-operator profiling and shortcuts.

Source: https://ducklocal.app/docs/sql-editor

**[中文](zh/sql-editor.md)**

Each query tab holds its own editor buffer. The editor highlights real SQL via
tree-sitter, indents with 2 spaces, and shows line numbers.

## Tabs

| Control | What it does |
| --- | --- |
| `+` or **⌘T** | Appends a new query tab and focuses it |
| Click a tab | Switches to it |
| **Ctrl+Tab** / **Ctrl+Shift+Tab** | The next / previous tab |
| **⌘1** … **⌘8**, **⌘9** | The first to eighth tab; ⌘9 is the last tab |
| Drag a tab | Moves it along the strip |
| `×`, **⌘W** or a middle click | Closes a tab. The last tab cannot be closed: its `×` is hidden and ⌘W does nothing. A dashboard with unsaved edits asks first |
| Right-click a tab | **Close tab**, **Close other tabs**, **Close tabs to the right**. Dashboards with unsaved edits stay open, and a notice says how many |
| Double-click a tab | Edits its title in place. Enter or clicking away keeps the new title, Escape keeps the old one; an empty title falls back to `Query N` |

Open tabs come back on the next launch, in their order: query tabs with their
titles and the SQL in their editors, whether or not it ran, and dashboard tabs.
Results do not — rerun the query. A query tab you open fresh starts with
`SELECT 'Hello, DuckLocal' AS greeting;`.

## The toolbar

Above the editor, from the left:

| Button | What it does |
| --- | --- |
| **Run** | Runs the editor's SQL — see [Running SQL](#running-sql) |
| **Stop** | Shown only while a query or a profile is running; interrupts it |
| **Format** | Reformats the SQL — see [Format](#format) |
| **Explain** | Shows the plan DuckDB would use, without running the statement |
| **Profile** | Runs the query and shows what each operator cost |

**Explain** and **Profile** are described in
[Explain and Profile](#explain-and-profile).

## Running SQL

Press **⌘↵** (Cmd+Enter) with the editor focused, or use the **Run** button.

While a query is running, pressing run again does nothing (nor do Explain and
Profile), and a **Stop** button appears beside Run. Stop, or **⌘.**, interrupts the query; it ends with
DuckDB's `Interrupted` error, recorded in history like any failure. There is no
timeout: without Stop, a query runs as long as DuckDB needs, and other database
work queues behind it.

Results stream from DuckDB, so a scan stops once the grid's
[row limit](results-and-charts.md#row-limits) is reached instead of reading
the rest of the file. A query that has to see every row before its first —
`ORDER BY`, `GROUP BY`, an aggregate, a window function — still computes in
full.

Each tab has its own results. A result that arrives after you switched tabs
lands in the tab that ran it.

How a statement is treated depends on its leading keyword. `SELECT`, `WITH`,
`SHOW`, `DESCRIBE`, `EXPLAIN`, `PRAGMA`, `SUMMARIZE`, `VALUES`, `FROM`,
`TABLE`, `PIVOT`, and `CALL` produce a result set. Anything else — DDL (data
definition) and DML (data modification) statements — reports `Done: N rows
affected, took …` instead of a grid. Leading `--`
comments are skipped when deciding; block comments are not.

### Scripts

The editor can hold several statements. **Run** executes all of them, in
order, the way the DuckDB shell reads a file, and **stops at the first
failure** — running a `SELECT` after a failed `ATTACH` would only add a second,
misleading error. A strip above the results lists the statements, each marked
with its row count, *done*, *failed* or *not run*; click one to see its result.
An error underlines the statement that failed, not the whole script.

A few of the shell's dot commands work too, each on a line of its own:

| Command | Shows |
| --- | --- |
| `.tables ?PATTERN?` | Tables and views, optionally filtered by an `ILIKE` pattern such as `stat%` |
| `.schema ?PATTERN?` | The `CREATE` statements of tables and views, optionally filtered |
| `.databases` | The attached databases |
| `.help` | This list |

Any other dot command (`.mode`, `.read`, …) is an error naming the ones that
work.

## Errors

A failed statement shows an alert titled *Query failed* with DuckDB's message
verbatim. The message comes from the engine and is always in English,
whatever the interface language is. Nothing is written to the grid, and the
attempt is still recorded in [history](schema-and-history.md) with a failure
marker.

When the message points at a position — parser and binder errors do, with a
`LINE n:` line and a caret — the editor underlines that spot with a red wavy
line: the token the caret marks, or the span a run of carets covers. The mark
follows your edits and clears on the next run. Runtime errors carry no
position and get no mark.

## Format

**Format** runs the buffer through `sqlformat` with a 2-space indent and
uppercased keywords, then replaces the entire buffer. It does nothing on an
empty buffer.

## Explain and Profile {#explain-and-profile}

Both buttons work on the editor's whole text, and both take **exactly one
statement**. With two or more — or with a dot command — they refuse with
*Explain and Profile take one statement: put the one to look at in a tab of its
own.* A trailing `;` and trailing comments are fine.

While one of Run, Explain or Profile is busy, the other two do nothing.
Neither Explain nor Profile is recorded in
[history](schema-and-history.md#query-history). Their output replaces the grid
on the **Results** tab, switching to it if another tab was open; a failure shows
the *Query failed* alert, as a failed run does.

### Explain

**Explain** runs `EXPLAIN <your statement>` and shows DuckDB's plan as plain
monospace text, with the time it took. It only plans: the statement is not
executed, so explaining an `INSERT`, `UPDATE` or `DELETE` is safe.

### Profile

**Profile** runs the statement under `EXPLAIN (ANALYZE, FORMAT JSON)` and shows
the plan that ran as a tree: each operator's own time and share of the total,
the rows it produced against the optimizer's estimate (flagged when off by more
than 10×), and what it filtered, joined on or projected. The operator that cost
the most is named at the top and drawn in red. How to read it:
[Explain and Profile output](results-and-charts.md#explain-and-profile).

**Profiling really runs the statement** — that is how its operators get timed.
So it accepts only a single read-only query:

- **Accepted:** `SELECT`, `WITH … SELECT`, `FROM …`, `VALUES`, `TABLE`, `SHOW`,
  `DESCRIBE`, `SUMMARIZE`.
- **Refused:** anything that writes (`INSERT`, `UPDATE`, `DELETE`,
  `CREATE`, …), `CALL`, `PRAGMA`, `COPY`, `EXPLAIN` itself, a write hidden
  behind a `WITH` (`WITH s AS (…) INSERT …`), and a `PIVOT` without an explicit
  `IN (…)` list.

A refused statement gets *Only statements that read can be profiled (SELECT,
WITH, FROM…): profiling runs the statement.* DuckDB's own parser decides this
before anything runs, not the first keyword.

A profile takes as long as the query, plus the cost of measuring it. **Stop**
(or **⌘.**) interrupts it like a run.

## Autocompletion

The completion menu is catalog-aware. Candidates are ranked:

1. Tables and columns from the schema (up to 35 entries)
2. DuckDB function names
3. SQL keywords

At most 50 items are offered. Matching is a case-insensitive **prefix** match
on the identifier immediately to the left of the cursor, so `sel` completes to
`SELECT` but `elect` completes to nothing. A function inserts its name and an
opening parenthesis; identifiers that contain spaces or special characters
are inserted double-quoted.

The keyword and function lists are a curated subset — not every DuckDB
function will appear.

## Keyboard shortcuts

On Windows and Linux, read **Ctrl** for **⌘** throughout.

DuckLocal's own shortcuts, all listed in the menu bar:

| Shortcut | Action |
| --- | --- |
| ⌘↵ | Run the query |
| ⌘. | Stop the running query |
| ⌘T | New query tab |
| ⌘W | Close the tab |
| Ctrl+Tab / Ctrl+Shift+Tab | Next / previous tab (Ctrl on a Mac too) |
| ⌘1 … ⌘9 | Go to the tab in that place; ⌘9 is the last |
| ⌘O | Open data |
| ⌘S | Save a dashboard's source, in its source view |
| ⌘B | Hide or show the sidebar |
| ⌘+ / ⌘− / ⌘0 | Make the interface larger / smaller / default size — see [Interface size](settings-and-data.md#interface-size) |
| ⌘Q | Quit |

The editor's own, which behave as in any text field:

| Shortcut | Action |
| --- | --- |
| ⌘F / ⇧⌘F | Find / replace in the editor |
| ⌘Z / ⇧⌘Z | Undo / redo |
| ⌘A | Select all |
| ⌘C / ⌘X / ⌘V | Copy / cut / paste |
| ⌥⌘↑ / ⌥⌘↓ | Add a cursor above / below |
| ⌘← / ⌘→ / ⌥← / ⌥→ | Move by line / word |
| ⌘↑ / ⌘↓ | Move to start / end of document |
| ⌘⌫ / ⌥⌫ | Delete to start of line / previous word |

## Menus

The menu bar holds every window command with its shortcut:

| Menu | Commands |
| --- | --- |
| DuckLocal | Command line & AI, Check for Updates…, Check for Updates at Launch, Show the guide, Open logs folder, Quit DuckLocal |
| File | New query, Open data, Save, Close tab, Next tab, Previous tab |
| Edit | Undo, Redo, Cut, Copy, Paste, Select all |
| Query | Run, Stop |
| View | Show or hide sidebar, Larger interface, Smaller interface, Default size, Online base map (OpenStreetMap) |

Windows and Linux have no menu bar; there, **Show the guide** and **Open logs
folder** are on the right-click menu of the version at the right end of the
status bar.

**Online base map (OpenStreetMap)** is a check item; see
[Charts](results-and-charts.md#charts). Rows in the schema sidebar have a
right-click menu too; see
[Schema and history](schema-and-history.md#right-click-menu). There is no
command palette.
