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.
On this page
Installation
Usage
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.
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.
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.
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.
<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}
/>States
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.
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
| Prop | Type | Notes |
|---|---|---|
query | string | The SQL the agent ran |
state | "running" | "done" | "error" | "canceled" | Defaults to done |
database | string | Database or connection name in the header |
columns | (SqlColumn | string)[] | { name, type?, format? }. Taken from the first object row when omitted |
rows | SqlRow[] | Arrays in column order or objects keyed by column name |
rowCount | number | Total rows when more exist than rows holds |
durationMs | number | Run time once finished |
startedAt | number | Epoch ms. Drives the live timer while running |
error | SqlResultError | string | { message, position?, severity?, detail?, hint?, code? } |
page, pageSize, onPageChange | number, number, (page) => void | 1-based paging. Controls show when all three are set |
maxHeight | number | string | Result scroll height. Defaults to 360 |
collapseAfter | number | false | Query lines before it collapses. Defaults to 6 |
truncateAfter | number | Characters before a cell truncates and opens in a popover. Defaults to 48 |
showRowNumbers | boolean | Row numbers in a gutter. Defaults to true |
exportFilename | string | Download CSV file name |
onDownload | () => void | Replaces the built-in download |
hideExport | boolean | Hide Copy as CSV and Download CSV |
onRetry | () => void | Retry query after an error, Run again after a cancel |
onCancel | () => void | Cancel query while running |
variant | "card" | "plain" | plain sits inside a surface you already have |
Keyboard
| Key | Action |
|---|---|
Tab | Moves through the query, the result table and each long cell |
Enter, Space | On a long cell, open its full value |
Escape | Close the full value and return focus to the cell |
| Arrow keys | Scroll the focused query or result table |
Accessibility
- The result is a real table with column headers, and
aria-rowcountandaria-rowindexcarry 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.