Skip to content
Neon UI
Esc
navigateopen⌘Jpreview
On this page

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

Query safety mode
Ln 1Col 1PostgreSQL

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. NULL stays distinct from an empty string.
  • Editing after a successful run keeps the result visible and marks it as Previous query until 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

PropType
onExecute(query: string, context: SQLExecutionContext) => Promise<SQLResult>

Executes SQL. The context carries the safety mode and an AbortSignal.

Type(query: string, context: SQLExecutionContext) => Promise<SQLResult>
value?string

Controlled SQL value.

Typestring
defaultValue?string

Initial SQL value when uncontrolled.

Typestring
Default""
onValueChange?(value: string) => void

Called when the editor changes.

Type(value: string) => void
mode?"read" | "read-write"

Controlled safety mode.

Type"read" | "read-write"
defaultMode?"read" | "read-write"

Initial safety mode when uncontrolled.

Type"read" | "read-write"
Default"read"
onModeChange?(mode: SQLRunnerMode) => void

Called when the safety mode changes.

Type(mode: SQLRunnerMode) => void
title?string

Editor heading.

Typestring
Default"SQL editor"
database?string

Connection context shown under the heading.

Typestring
Default"neondb"
disabled?boolean

Disables editing and execution.

Typeboolean
Defaultfalse

Result 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.