Here is a two-line statement. Highlight the first line, press the run shortcut, and watch the table empty.
DELETE FROM orders
WHERE status = 'cancelled';
Nothing malfunctioned. Running a selection means "treat the highlighted text as the whole
script", and the highlighted text is DELETE FROM orders — a complete, valid,
unqualified statement. Every editor worth using behaves this way, and every one of them
hands you this particular loaded foot-gun for free.
The Feature Is Not the Bug
The obvious fix is to stop executing partial selections. It is also the wrong fix. Running
one statement out of a script of twenty is the reason the shortcut exists — you iterate on a
query, you re-run a single UPDATE after tweaking a value, you check the
SELECT above the DELETE before committing to it. Take that away and
people go back to a scratch file, which is worse in every direction.
What is actually wrong is the asymmetry. A truncated SELECT is a syntax error
and costs you nothing. A truncated DELETE or UPDATE is still
grammatical, and the part you cut off was the part limiting the damage. The dangerous case
is the one the parser is happiest with.
Warn, Do Not Forbid
So we kept the behaviour and put a gate in front of it. Every path that can send a script — the run button, the run-selection button, and each keyboard shortcut — now goes through a single choke point that scans the exact text about to be transmitted and, if it finds something destructive, shows you what it found before anything leaves the machine.
That "each keyboard shortcut" clause is not filler. Our first version guarded the buttons
and left the keybindings wired straight to the executor, which meant the dialog appeared for
everyone except the people fast enough to use shortcuts — precisely the users running
DELETE statements from memory. One entry point, or you have not built a safety
feature; you have built a placebo.
A Scanner, Not a Parser
Recognising destructive SQL sounds like a job for a real parser, and it is not — five engines means five dialects, and a parser that is 98% correct fails in exactly the situations you built it for. We wrote a scanner instead, tuned to be loud: it masks string literals and comments so keywords cannot hide inside them, splits the script into statements, and classifies each one. A false alarm costs one click. A miss costs a table.
Four things in that paragraph turned out to be harder than they read.
Double quotes are not strings
The first version masked anything between quotes, single or double. In standard SQL a double
quote is identifier quoting, so masking it erased the table name — and
DROP TABLE "orders" classified as nothing at all and ran with no warning. The
fix is one character wide: only ' opens a string. Double quotes and backticks
stay, because that is where the name lives.
T-SQL's GO is a batch separator
Split a script on semicolons and this is one blob that starts with USE:
USE Shop
GO
DELETE FROM Orders
Every pattern anchored at the start of a statement then looks at USE Shop GO DELETE…
and sees a harmless USE. GO is not SQL and not terminated by a
semicolon — it is a client-side batch separator on its own line, and a scanner that does not
know about it goes blind after the first one.
MERGE always warns
A MERGE can update or delete every matched row inside its WHEN
clauses, and there is no way to tell which without parsing the entire statement — the thing we
explicitly chose not to build. So it always warns. This is the tuning working as intended:
when the scanner cannot be sure, it is loud.
An apostrophe inside a regex hid every drop
The MongoDB side needed the same masking, plus regex literals, because JavaScript has them
and SQL does not. Miss that and the apostrophe in
find({ tag: /can't stop/ }) opens a string that never closes, and every
drop() later in the script disappears from view.
Telling a regex from a division means looking at the previous meaningful character, and ours
started out as an empty string. In JavaScript, ''.includes('') is
true, so a regex at position zero read as division — and one script-opening regex
was enough to blind the scanner for the rest of the file. A scanner whose entire job is not
missing a drop, missing every drop. It now starts as a space.
Why SQL Gets a Dialog and MongoDB Gets a Journal
Sutido snapshots MongoDB deletes into a local changelog before they run, so a wrong
deleteMany is
recoverable per document. No SQL
engine offers a client anything comparable — once DELETE FROM orders is
committed, the client has nothing left to offer but sympathy.
That difference decides the design. Where we can undo, we capture quietly and stay out of the
way. Where we cannot, we spend a click of your attention up front and name what is about to
happen: which statement, which table, and whether it has a WHERE clause at all.
The same rule applies to the MongoDB operations the journal does not cover —
drop(), dropDatabase(), and deletes issued through
runCommand all stop and ask.
Nothing here is clever. It is the boring conclusion that safety features have to sit on the last line before the wire, cover every entry point equally, and be tuned to annoy you rather than to be right.
Five Engines, One Set of Guardrails
MongoDB, PostgreSQL, SQL Server, Aerospike and ClickHouse — in one client that reads what you are about to run.
Download Now