SQLRunner
A guarded SQL editor with run and cancel controls, inline Postgres errors, and a typed results grid.
SQLRunner combines a PostgreSQL-aware CodeMirror editor, execution controls, Postgres feedback, and a typed results grid. Read-only mode blocks statements that can change data before they reach your handler. Read + write mode stays visible in amber. Cmd/Ctrl+Enter runs the selection (or the full document); Escape cancels an in-flight request.
SQL editor
production / neondb
Results
Run a query to see results.
Read-only mode blocks statements that can change data.
Behavior
- Select any statement to run only that SQL. With no selection, Run executes the full document.
- PostgreSQL syntax highlighting, bracket matching, history, search, line numbers, and line/column feedback come from CodeMirror 6.
- Run with the button or Cmd/Ctrl+Enter. The button becomes Cancel while the request is active.
- Read-only mode blocks mutating statements locally and explains why. Enabling writes is explicit and persistent.
- Errors mark the exact editor position with a gutter diagnostic and underline, then stay beside the editor with the Postgres code, line, column, detail, and hint.
- Results preserve column order and types.
NULLstays distinct from an empty string. - Editing after a successful run keeps the result visible and marks it as
Previous queryuntil the next run. - Horizontal and vertical result overflow use the Neon scroll fade.
Install
npx shadcn@latest add https://ui.neon.com/r/sql-runner.json
Usage
import { SQLRunner } from "@/components/neon-ui/sql-runner";
import { useSQLRunner } from "@/hooks/use-sql-runner";
const { execute } = useSQLRunner({ endpoint: "/api/sql" });
<SQLRunner
database="production / neondb"
defaultMode="read"
defaultValue="select now();"
onExecute={execute}
onModeChange={(mode) => rememberMode(mode)}
/>;
The safety mode is uncontrolled and starts at read, so the runner is read-only until someone turns writes on. onModeChange fires on every switch, which is where you persist the choice or log it. Pass mode instead of defaultMode to drive it yourself, e.g. to pin a production connection to read-only and never offer writes at all.
Wiring to Neon
The included useSQLRunner hook posts { query, mode } to an endpoint and forwards its AbortSignal. The mode travels with the query so the server can enforce it: a read request runs inside a READ ONLY transaction, and Postgres rejects a write with 25006 whether or not it came from the component.
import { neon } from "@neondatabase/serverless";
export async function POST(request: Request) {
const { query, mode } = await request.json();
// fullResults on the client, so the transaction overload returns field
// metadata and a command tag rather than bare rows.
const sql = neon(process.env.DATABASE_URL, { fullResults: true });
const startedAt = performance.now();
const [result] =
mode === "read"
? await sql.transaction([sql.query(query)], { readOnly: true })
: [await sql.query(query)];
return Response.json({
command: result.command,
durationMs: performance.now() - startedAt,
fields: result.fields.map((field) => ({
name: field.name,
type: String(field.dataTypeID),
})),
rowCount: result.rowCount,
rows: result.rows,
});
}
The classifier in the component is interaction feedback: it stops a mutating statement before it costs a round trip and explains why. It is not a security boundary, and a request can reach the endpoint without it. Authenticate the endpoint, and treat the READ ONLY transaction (or a read-only Postgres role) as the thing that actually enforces the mode.
Props
onExecute(query: string, context: SQLExecutionContext) => Promise<SQLResult>
Executes SQL. The context carries the safety mode and an AbortSignal.
(query: string, context: SQLExecutionContext) => Promise<SQLResult>value?string
Controlled SQL value.
stringdefaultValue?string
Initial SQL value when uncontrolled.
string""onValueChange?(value: string) => void
Called when the editor changes.
(value: string) => voidmode?"read" | "read-write"
Controlled safety mode.
"read" | "read-write"defaultMode?"read" | "read-write"
Initial safety mode when uncontrolled.
"read" | "read-write""read"onModeChange?(mode: SQLRunnerMode) => void
Called when the safety mode changes.
(mode: SQLRunnerMode) => voidtitle?string
Editor heading.
string"SQL editor"database?string
Connection context shown under the heading.
string"neondb"disabled?boolean
Disables editing and execution.
booleanfalseResult shape
interface SQLResult {
rows: Record<string, unknown>[];
fields?: { name: string; type?: string }[];
rowCount?: number;
durationMs?: number;
command?: string;
}
Accessibility
The CodeMirror editor carries an accessible name, visible focus, native text selection, search, history, and keyboard navigation. Cmd/Ctrl+Enter runs the current selection or document; Escape cancels. Errors use an assertive live region; execution and result metadata use a polite live region. The loading spinner stops under reduced motion.