Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 46 additions & 12 deletions docs/editor/query-optimization.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,8 @@ same reading the auto-limiter uses, so the two cannot disagree about where a sta
| The `DO` of `ON CONFLICT DO NOTHING` or `CREATE RULE … DO INSTEAD`, which a word follows | No |
| Any of those behind a comment (`-- note`, `/* note */`, MySQL's `# note`, several stacked) | Yes |
| A `WITH` whose CTE list precedes one (`WITH x AS (…) DELETE FROM …`) | Yes |
| A `WITH` whose CTE list *contains* an `UPDATE … SET` (PostgreSQL's data-modifying CTE) | Yes |
| A `WITH` whose CTE list *contains* an `INSERT`, `UPDATE … SET`, `DELETE` or `MERGE` (PostgreSQL's data-modifying CTE; CockroachDB shares the grammar) | Yes |
| `EXPLAIN ANALYZE` or `EXPLAIN (ANALYZE, …)` of a statement that asks, on PostgreSQL (CockroachDB shares the grammar). `ANALYZE` set to a boolean false (`false`, `off`, `no`, `0` and their prefixes) or an integer 0 (`0x0`, `0b0`, `0o0`, `0_0`), and a plain `EXPLAIN`, only plan. `0x1`, `0b1`, `0o1` and `0_1` execute | Yes, the same answer as the explained statement |
| A statement carrying a run the reader cannot resolve (`SELECT '\';` and anything after it, an unterminated comment, literal, name or array) | Yes, and the dialog says why |
| Any of those behind a **nested** comment (`/* a /* b */ c */ DROP TABLE users`), on a dialect that nests — PostgreSQL, SQL Server, ClickHouse | Yes: the comment is read whole, so the statement's own keyword is the one that answers |
| The same text on a dialect that does **not** nest — MySQL, MariaDB, SQLite, Oracle | No, because there the comment closed at the first `*/` and what follows it is text the engine rejects outright |
Expand All @@ -595,12 +596,36 @@ Writing a note above a destructive statement is ordinary, and it is exactly wher
matters most.
┌──────────────────────────────────────────────────────────────────────────────┐
One probe stays deliberately unanchored: a statement whose code contains `UPDATE` followed by
`SET` asks for confirmation whatever its own keyword is. PostgreSQL's data-modifying CTE is
`SET` asks for confirmation whatever its own keyword is, except an `EXPLAIN` that will not execute
it (below). PostgreSQL's data-modifying CTE is
*operated* by its `SELECT` — which is the honest answer, and the one the limiter needs so it does
not bound the rows a write commits — so anchoring this probe too would take the last check away
from a statement that really does write. It reads the statement's **code** rather than its text
(`src/lib/sql/words.ts`), so unlike the pattern before it, a read that merely quotes
`'UPDATE t SET x = 1'` or mentions it in a comment no longer prompts.

`DELETE`, `MERGE` and `INSERT` are the other three words of that set (`DATA_MODIFYING` in
`src/lib/explain/select-prefix.ts`). They count only when the statement leads with `WITH`, the CTE
list can be read, and the word sits in that list — before the keyword that operates the statement —
so `WITH d AS (DELETE FROM t RETURNING *) SELECT * FROM d` asks, and so do the `MERGE … DELETE` and
`INSERT` forms. CockroachDB connections are the `postgres` type-id and share that grammar, so they
ask too. A plain `SELECT` whose code names one of those words does not ask, and neither does a
word that only appears in a string or a comment, or after the statement the list precedes. An
unclosed list still does not: the shape cannot be typed, and no dialect accepts the text.
┌──────────────────────────────────────────────────────────────────────────────┐
On PostgreSQL, `EXPLAIN ANALYZE` executes the statement it explains. Typed into the editor, it used to
be read as an `EXPLAIN`, which is not a dangerous keyword, so `EXPLAIN ANALYZE DELETE FROM users`
deleted the rows with no confirmation (#1863). The gate strips a leading run of `ANALYZE`, `ANALYSE`
and `VERBOSE`, or a parenthesised option list, and when `ANALYZE` is enabled asks about the remaining
statement the way it would on its own — including a data-modifying CTE written under the `EXPLAIN`.
`ANALYZE` with no value is on; `true`, `on` and `1` are on. A value is off only when the whole of it
is a boolean false (`false`, `off`, `no`, `0` and their accepted prefixes) or an integer literal of
value 0, so `0x0`, `0b0`, `0o0` and `0_0` plan while `0x1`, `0b1`, `0o1` and `0_1` execute. Anything
else is on. A plain
`EXPLAIN`, `EXPLAIN VERBOSE`, or `ANALYZE` set off only plans, so it does not ask, and an `UPDATE … SET`
inside that plan does not either. A word inside a string or a comment is not an option and is not the
statement. CockroachDB connections are the `postgres` type-id and share the grammar, so they ask too.
The grammar fact is `explainExecutesWithAnalyze`, absent on every other row.
┌──────────────────────────────────────────────────────────────────────────────┐
### Text the reading cannot resolve asks, and says so
┌──────────────────────────────────────────────────────────────────────────────┐
Expand Down Expand Up @@ -663,23 +688,31 @@ own grammar (#295) and unresolvable only to a reader without it — and it lifts
cannot read at all: Oracle's `q'{it's}'` (#292). It does nothing for the first class, per that
bullet.
┌──────────────────────────────────────────────────────────────────────────────┐
Two gaps are known and pinned by tests rather than claimed closed:
One gap is known and pinned by tests rather than claimed closed:
┌──────────────────────────────────────────────────────────────────────────────┐
- **Only `UPDATE … SET` is looked for inside a read.** A write hidden in a CTE *body* under any
other keyword (`WITH gone AS (DELETE FROM t RETURNING *) SELECT * FROM gone`) does not prompt.
Widening the probe to `DELETE`/`INSERT`/`MERGE` would also make every read whose code names one
of them prompt, so the asymmetry is inherited from the vocabulary above rather than chosen here.
- **A statement whose shape cannot be TYPED** — an unclosed CTE list such as
`WITH t AS (DELETE FROM x` — does not prompt. This is the boundary of the rule above rather than
an exception to it: every character was read, and what they spell is an incomplete statement
rather than a run hiding what is written inside it. No dialect accepts the text, so the server
rejects it; and the keyword inside the unclosed list is a CTE-body write, which the gap above
already does not prompt for even when the list closes. Pinned by
rejects it. The same keyword inside a list that *does* close asks (#1862); this one does not,
because the list cannot be crossed and the probe refuses to guess. Pinned by
`tests/components/QuerySafetyDialog.test.tsx` ("does not prompt when the statement's shape cannot
be typed") so the boundary stays a decision — the shipped rule keys on unresolvable *runs*, and
widening it to every statement the reader cannot type would prompt for an empty editor.
┌──────────────────────────────────────────────────────────────────────────────┐
A third gap sat beside them and is closed (S1). A multi-statement script used to be read as one
A data-modifying CTE that `DELETE`s, `MERGE`s or `INSERT`s used to be a second gap beside that one:
only `UPDATE … SET` was looked for inside a read, so
`WITH gone AS (DELETE FROM t RETURNING *) SELECT * FROM gone` ran with no confirmation. Widening the
unanchored pair to those words would have made every read whose code names one of them prompt, so
the three are read only inside a `WITH` whose list can be typed. That gap is closed (#1862).
┌──────────────────────────────────────────────────────────────────────────────┐
`EXPLAIN ANALYZE` of a write was the same kind of gap: the operative keyword is `EXPLAIN`, so a
`DELETE` or `DROP` under it never asked, while `EXPLAIN (ANALYZE) UPDATE … SET` asked only because
the unanchored pair happened to see the `UPDATE`. The explained statement is what runs, so the gate
reads it as its own statement when `ANALYZE` is on, and stays silent when `ANALYZE` is off or absent
(#1863). Pinned by `tests/components/QuerySafetyDialog.test.tsx`.
┌──────────────────────────────────────────────────────────────────────────────┐
Another gap sat beside them and is closed (S1). A multi-statement script used to be read as one
statement, so `SELECT 1; DROP TABLE users` did not prompt — its first keyword is the `SELECT` —
while `SELECT 1; UPDATE t SET x = 1` did, because the unanchored probe happened to find it. The gate
now splits the editor text with the same splitter and the same grammar the runner uses, and asks
Expand All @@ -689,8 +722,9 @@ in a Mongo document or a Redis command separates nothing, so a fragment cut ther
run").
┌──────────────────────────────────────────────────────────────────────────────┐
Four runs bypass the gate on purpose, all on the standalone path
(`src/hooks/use-query-execution.ts`): an EXPLAIN run (see below), a Load-More page of a result the
user already has, a playground run (rolled back rather than committed), and anything carrying
(`src/hooks/use-query-execution.ts`): an EXPLAIN run built by the Explain button (see below —
`EXPLAIN ANALYZE` typed in the editor is an ordinary run and reaches the gate), a Load-More page of a
result the user already has, a playground run (rolled back rather than committed), and anything carrying
`skipSafety` — which is the dialog's own "Execute Anyway" button and the inline row editor, whose
`UPDATE` comes from a grid edit the user has already confirmed. The embedded adapter
(`src/workspace/hooks/use-query-adapter.ts`) has only the force-execute bypass, so every other
Expand Down
3 changes: 3 additions & 0 deletions docs/providers/postgres.md
Original file line number Diff line number Diff line change
Expand Up @@ -1820,6 +1820,8 @@ Before this, the same file stopped at its first row with `ERROR: malformed array
The editor's confirmation gate asks before the shared destructive keywords, `MERGE` among them ([query-optimization.md](../editor/query-optimization.md#destructive-statement-confirmation)).
On this dialect it also asks before a `DO` block, whatever the block does: the block's code is a literal, `$$ ... $$` or `'...'`, which the gate does not read.
The grammar row's `confirmCodeForms` names `DO` followed by a body and `DO LANGUAGE` ([`postgres-sql-grammar.ts`](../../src/lib/db/providers/sql/postgres-sql-grammar.ts)), so the `DO` of `INSERT ... ON CONFLICT DO NOTHING` and of `CREATE RULE ... DO INSTEAD`, which a word follows, does not ask.
`explainExecutesWithAnalyze` is set, because `EXPLAIN ANALYZE` (and `EXPLAIN (ANALYZE, …)`) runs the explained statement: the gate strips the option and asks about what remains when `ANALYZE` is on. A plain `EXPLAIN` only plans, and so does `ANALYZE` set to a boolean false (`false`, `off`, `no`, `0` and their accepted prefixes) or to an integer literal of value 0 (`0x0`, `0b0`, `0o0`, `0_0`). A nonzero literal (`0x1`, `0b1`, `0o1`, `0_1`) executes, so it asks.
CockroachDB shares this type id and this grammar.

---

Expand All @@ -1834,6 +1836,7 @@ Block comments nest, per the PostgreSQL manual (4.1.5 Comments, checked 2026-08-
The row bound goes before Materialize's `AS OF [AT LEAST] <t>`, which shares this type id (E2E pass of 2026-10-03/04, #1398): `SELECT * FROM ui_mv AS OF AT LEAST 0` with the bound appended is `Expected end of statement, found LIMIT`, and with the bound before the clause it returns rows.
Completion quotes a catalog name by PostgreSQL's `quote_ident` rule, with the keyword set taken from PostgreSQL 18's `kwlist.h`: a name stays bare only when it is lower case and not a keyword other than an unreserved one, and a qualifier written in double quotes before the dot is read back with its doubled quotes undone.
Only PostgreSQL completions introduce quoted table names; every other dialect quotes through the connection's capabilities.
`explainExecutesWithAnalyze` is `true`: `EXPLAIN ANALYZE` executes, and the confirmation gate reads the explained statement ([§5.8](#58-the-confirmation-gate)).
`readsSqlText` is `true`: the editor text is SQL, and the provider extends `SQLBaseProvider`.
Redaction is `sql` with `"` as its name quote: a redacted statement keeps its words and every name in that quote, and every literal becomes `?`.

Expand Down
Loading