Skip to content
Sign in
DataSQL resultMenu

SQL result

A query the agent ran and what came back: highlighted SQL, row count and run time, a result table with a sticky header, NULL badges, full values for long cells, paging, CSV export and the running, empty, error and canceled states.

March churn by account: highlighted query, row count and run time, NULL badges, numbers in tabular figures and long reasons that open in full.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/sql-result.json

Usage

tsx
import { SqlResult } from "@/components/ui/chatcn/sql-result"

<SqlResult
  database="analytics"
  query={part.input.sql}
  columns={part.output.fields}
  rows={part.output.rows}
  durationMs={part.output.durationMs}
/>

Rows can be arrays in column order or objects keyed by column name, the two shapes database drivers return. Column types are inferred from the values: numbers align to the end in tabular figures, null shows a quiet NULL badge and objects render as JSON. Pass columns with a type or a format function when you know better, for example to show cents as money.

tsx
const columns = [
  { name: "account" },
  { name: "mrr_lost", format: (v) => Number(v).toFixed(2) },
  { name: "account_id", type: "text" },
]

Map the tool state to state: running while the query executes (pass startedAt for the live timer and onCancel for Cancel query), done with rows, error with the database error, and canceled.

Examples

Paged result

Set rowCount to the full total, and page, pageSize and onPageChange to page through it on your server. Without paging props, a result with fewer rows than rowCount says Showing 50 of 2,341 rows.

One page of 2,341 rows with a sticky header, row numbers and Previous and Next.

Long query

Queries over six lines collapse behind Show full query so the result stays in view. Change the limit with collapseAfter, or pass false to always show everything.

A cohort retention CTE collapses behind Show full query, so the result stays in view.

Database error

Pass error with the message and the position the database reports (1-based, as Postgres does). The failing name gets a red underline, a caret is drawn under it like psql, and the line and column are spelled out. Put the database's hint in so the error says how to fix it.

tsx
<SqlResult
  state="error"
  query={sql}
  error={{
    message: "column s.plan_tier does not exist",
    position: 18,
    code: "42703",
    hint: 'Perhaps you meant to reference the column "s.plan".',
  }}
  onRetry={rerun}
/>
The database message, a caret under the failing column, the SQLSTATE and the hint, with Retry query.

States

Running with a live timer and Cancel query, no rows, and canceled.

Export

Copy as CSV and Download CSV export the rows you passed. To export the full result instead, pass onDownload and stream the file from your server. The helpers are exported too.

tsx
import { downloadCsv, sqlResultToCsv } from "@/components/ui/chatcn/sql-result"

// RFC 4180: quoted commas, quotes and line breaks, NULL as an empty field, CRLF lines.
const csv = sqlResultToCsv(columns, rows)
downloadCsv(csv, "march-2026-churn.csv")

Props

PropTypeNotes
querystringThe SQL the agent ran
state"running" | "done" | "error" | "canceled"Defaults to done
databasestringDatabase or connection name in the header
columns(SqlColumn | string)[]{ name, type?, format? }. Taken from the first object row when omitted
rowsSqlRow[]Arrays in column order or objects keyed by column name
rowCountnumberTotal rows when more exist than rows holds
durationMsnumberRun time once finished
startedAtnumberEpoch ms. Drives the live timer while running
errorSqlResultError | string{ message, position?, severity?, detail?, hint?, code? }
page, pageSize, onPageChangenumber, number, (page) => void1-based paging. Controls show when all three are set
maxHeightnumber | stringResult scroll height. Defaults to 360
collapseAfternumber | falseQuery lines before it collapses. Defaults to 6
truncateAfternumberCharacters before a cell truncates and opens in a popover. Defaults to 48
showRowNumbersbooleanRow numbers in a gutter. Defaults to true
exportFilenamestringDownload CSV file name
onDownload() => voidReplaces the built-in download
hideExportbooleanHide Copy as CSV and Download CSV
onRetry() => voidRetry query after an error, Run again after a cancel
onCancel() => voidCancel query while running
variant"card" | "plain"plain sits inside a surface you already have

Keyboard

KeyAction
TabMoves through the query, the result table and each long cell
Enter, SpaceOn a long cell, open its full value
EscapeClose the full value and return focus to the cell
Arrow keysScroll the focused query or result table

Accessibility

  • The result is a real table with column headers, and aria-rowcount and aria-rowindex carry the position inside a paged result.
  • NULL reads as null to screen readers, and long cells are buttons named with the column and the start of the value.
  • The query and the table are focusable scroll regions with labels, so keyboard users can scroll them.
  • The outcome is announced once through a polite live region, for example Query returned 10 rows. The timer is never announced.
  • Status never rides on color alone: each state has its own glyph and title. The error position is spelled out as a line and column, not only drawn.
  • SQL always reads left to right, while the header, table and footer follow the page direction.

Motion

  • The status glyph swaps with the kit's 150 ms blur crossfade. Under reduced motion it simply changes.
  • The full value popover grows from its cell in 180 ms and opens without animation from the keyboard.
  • Expanding the query, paging and new rows are instant: this is a tool you read, not a show.