SQL Guard

Warns before running SQL that touches every row or drops data, like UPDATE/DELETE without WHERE, and asks for the table name first.

StopsHelps catch a DELETE without WHERE before it runs

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/sql-guard.json

The files land in your project. They're yours to edit.

Usage

sql-guard-usage.tsx
import { SqlGuard, type SqlStatementInfo } from "@/components/ui/sql-guard"

<SqlGuard
  env="production"
  defaultValue={"UPDATE users SET plan = 'free'\n-- WHERE id = 4182;"}
  estimateRows={estimate}
  onRun={(sql) => setLog(`Ran ${sql.split(/\s+/)[0].toUpperCase()} on production.`)}
/>

What it catches

  • UPDATE or DELETE with no top-level WHERE
  • A WHERE that is only inside a -- or /* */ comment, with a one-tap uncomment
  • Simple always-true conditions after WHERE or OR (1=1, TRUE, id = id, 'a'='a')
  • DROP TABLE/DATABASE/SCHEMA and TRUNCATE
  • Any write when env is in productionEnvs
  • Estimated affected rows from your estimateRows

Props

SqlGuard props
PropTypeDefault
value

Controlled query text.

string—
defaultValue

Initial query when uncontrolled.

string""
onValueChange

Called with the query text on every change.

(value: string) => void—
onRun* (required)

Runs the query. Return a promise to show a pending state; a rejection is shown under the box.

(sql: string) => void | Promise<void>—
env

Connection the query runs against, e.g. "production" or "staging".

string—
productionEnvs

Environments that ask before any write.

string[]["production", "prod"]
estimateRows

Estimates how many rows a write statement touches. Return null when unknown.

(statement: SqlStatementInfo) => number | null | Promise<number | null>—
label

Visible label.

string"Query"
runLabel

Label of the Run button.

string"Run"
runAnywayLabel

Label of the confirm-run button.

string"Run anyway"
cancelLabel

Label of the Cancel button.

string"Cancel"
productionText

`{env}` is filled in.

string"This runs on {env}."
typeTableText

`{table}` is filled in.

string"Type {table} to run it."
uncommentLabel

Label of the uncomment-WHERE fix.

string"Uncomment WHERE"

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Visible <label>; Cmd/Ctrl+Enter runs; hints in a polite live region via aria-describedby
  • Confirmation is a labelled group; focus moves into it, Escape closes
  • Type-the-table input has a <label>; Enter runs only when it matches
  • Environment shown as text with a colour dot

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Run with least-privilege credentials, prefer transactions and read-only connections, and enforce production write approval on the server.

  • Pattern checks only: it does not parse full SQL or prevent SQL injection, and a query with a WHERE can still be destructive
  • Tokenizer understands --, # (MySQL) and /* */ comments, '…' and $$…$$ strings, and "…", `…` and […] identifiers; "#>" and "#-" are read as Postgres operators, but a bare Postgres "#" (bitwise XOR) is read as a comment
  • Always-true checks look at a single term right after WHERE or OR ("WHERE 1=1", "OR 1=1", "OR id = id"); a term followed by AND ("WHERE 1=1 AND …", the query-builder pattern) is not flagged, and tautologies inside parentheses or longer expressions are missed
  • Not dialect-aware beyond that; CTEs are judged by their writing verb

What it stops

Helps catch a DELETE without WHERE before it runs

Also known as

sql · query editor · delete · update · where clause · drop table · database console · admin

Field type

Dependencies

Pairs well with