Interactive charts
Line and bar charts with column selectors. Powered by Recharts, capped at 500 rows for smooth SVG rendering.
A database is only as useful as its query interface. ZeptoDB’s web UI started as a single text box that returned a table. This post covers three enhancements that bring it closer to a real analytical workbench: chart visualization, multi-tab editing, and multi-statement execution — all implemented in a single pass.
Query results are most useful when you can see them. The editor now supports toggling between table and chart views, with line and bar chart types powered by Recharts.
The user selects:
// ResultChart.tsx — core rendering<ResponsiveContainer width="100%" height={400}> {chartType === 'line' ? ( <LineChart data={chartData}> <XAxis dataKey={xColumn} /> <YAxis /> {yColumns.map(col => ( <Line key={col} dataKey={col} dot={false} /> ))} </LineChart> ) : ( <BarChart data={chartData}> <XAxis dataKey={xColumn} /> <YAxis /> {yColumns.map(col => ( <Bar key={col} dataKey={col} /> ))} </BarChart> )}</ResponsiveContainer>Recharts renders SVG elements — one <circle> or <rect> per data point per series. At 10,000 rows with 3 Y columns, that’s 30,000 SVG elements, which tanks browser performance. The chart view caps at 500 rows, which keeps rendering smooth while showing enough data for visual analysis.
The view mode (table vs chart) is a global toggle, not per-result-tab. This is simpler UX — when a user switches to chart mode, they typically want to see all results as charts, not manage view state per tab.
The editor now supports multiple tabs, each with independent code and results.
Tab state persists across page reloads via localStorage:
// Saved to localStorage on every changeinterface TabState { id: string; name: string; code: string; // results intentionally excluded}Results are not persisted — they can be large (thousands of rows) and would bloat localStorage. On reload, tabs restore their code but results are cleared. This is the right tradeoff: code is small and valuable, results are large and reproducible.
The editor now supports executing multiple SQL statements in a single run. Statements are split on ; and executed sequentially.
// Split on ; while respecting single-quoted stringsfunction splitStatements(sql: string): string[] { const statements: string[] = []; let current = ''; let inQuote = false;
for (const ch of sql) { if (ch === "'" && !inQuote) inQuote = true; else if (ch === "'" && inQuote) inQuote = false; else if (ch === ';' && !inQuote) { if (current.trim()) statements.push(current.trim()); current = ''; continue; } current += ch; } if (current.trim()) statements.push(current.trim()); return statements;}Each statement executes independently. Results appear as sub-tabs in the results area:
┌─────────────────────────────────────────────┐│ Tab: "Analysis" │├─────────────────────────────────────────────┤│ SELECT * FROM trades WHERE sym='AAPL'; ││ SELECT avg(price) FROM trades GROUP BY sym; ││ SELECT count(*) FROM orders; │├─────────────────────────────────────────────┤│ Results: [Statement 1] [Statement 2] [3] ││ ││ sym │ avg_price ││ AAPL │ 152.34 ││ MSFT │ 287.91 │└─────────────────────────────────────────────┘If statement 2 fails, statements 1 and 3 still show their results. The error is displayed inline for the failed statement — not all-or-nothing.
The ; splitter handles single-quoted strings but does not handle $$ dollar-quoting or -- comments containing semicolons. This is sufficient for ZeptoDB’s SQL dialect, which doesn’t use dollar-quoting. A full SQL-aware parser would add complexity for an edge case that doesn’t exist in practice.
| File | Change |
|---|---|
web/src/app/query/page.tsx | Rewritten: tab state, multi-statement run(), result sub-tabs, chart/table toggle |
web/src/components/ResultChart.tsx | New: line/bar chart with X/Y column selectors |
The existing query.test.ts (3 tests) passes unchanged — the loadHistory export was preserved through the rewrite.
Interactive charts
Line and bar charts with column selectors. Powered by Recharts, capped at 500 rows for smooth SVG rendering.
Multi-tab editing
Independent code and results per tab. localStorage persistence for code, cleared results on reload.
Multi-statement run
Split on semicolons, execute sequentially, per-statement error handling. No all-or-nothing failures.
Zero build errors
next build passes with zero TypeScript errors. Existing tests unchanged.
Related: HTTP Observability → · SQL Parser & HTTP JOIN → · Python Ecosystem Integration →