SQL Workspace
Multi-tab SQL editor with streaming results, result filtering & sorting, automatic query history, error navigation, data upload, and saved queries
CH-UI provides a ClickHouse-first SQL workspace with a multi-tab editor, schema explorer, autocomplete, query streaming, result filtering and sorting, automatic query history, error navigation, data upload, and saved queries.
Overview
The workspace is organized around a tab system with two split groups (left/right). Each tab can be a query editor, table browser, or singleton page.
Multi-Tab Editor
The SQL editor uses CodeMirror 6 with ClickHouse syntax highlighting and schema-aware autocomplete.
Tab Types
| Type | Behavior |
|---|---|
| Query | Multi-instance, supports dirty tracking, SQL persisted in localStorage |
| Table/Database | Context-specific browsing with data preview |
| Singleton | Home, SavedQueries, Dashboards, Brain, Admin, etc. — one instance each |
Persistence
Tabs are saved to localStorage (ch-ui-tabs) with auto-save via microtask debounce. Tab state survives browser refreshes and includes:
- Tab ID, type, and name
- SQL content and dirty flag
- Saved query reference (if linked)
- Split group assignment (left/right)
Split View
Open up to two editor groups side by side. Tabs can be moved between groups. Duplicate detection prevents opening the same saved query twice.
Query Execution
Standard Execution
POST /api/query/run{
"query": "SELECT * FROM system.tables LIMIT 20",
"timeout": 30,
"maxResultRows": 1000
}| Parameter | Description | Default | Max |
|---|---|---|---|
query | SQL to execute | Required | — |
timeout | Seconds before timeout | 30 | 300 (5 min) |
maxResultRows | Server-side row limit sent to ClickHouse | 1000 | — |
Query Streaming
For large result sets, use SSE-based streaming:
POST /api/query/streamReturns chunked NDJSON with message types:
| Type | Content |
|---|---|
meta | Column names and types |
chunk | Data rows (with sequence number) |
done | Statistics and total row count |
error | Error message |
Stream limit: 1,000,000 rows.
EXPLAIN & Profiling
| Endpoint | Purpose |
|---|---|
POST /api/query/explain | Raw EXPLAIN output |
POST /api/query/plan | Parsed query plan as tree (tries EXPLAIN PLAN, falls back to EXPLAIN AST, then generic) |
POST /api/query/profile | Latest system.query_log metrics for the exact query (duration, read rows/bytes, memory) |
POST /api/query/sample | First N rows per shard (default 25, max 500), falls back to global LIMIT |
Working with Results
Sorting & Filtering
Click any column header to sort, or use the per-column filter popovers (type-aware operators: contains, equals, comparisons, NULL checks). Active filters show as removable chips above the grid.
CH-UI picks the cheapest correct strategy automatically:
- Complete result in memory (under the row limit and ≤ 50k rows) — sorting and filtering happen instantly client-side.
- Truncated or huge results — the original query is re-run on the server wrapped as
SELECT * FROM (your query) WHERE … ORDER BY …, BigQuery-style, so you get the true top-N over the full dataset instead of a misleading sort of the loaded page. The footer marks these views as server-filtered.
Exports always reflect the filtered/sorted view you're looking at. The feature can be toggled off in the result footer.
Query History
Every query you run from the editor is recorded automatically — full SQL, success/error/cancelled status, duration, and row count. Open it with the History button in the editor toolbar:
- Search and filter (all / success / errors)
- Open any entry in a new tab, copy it, or replace the current editor contents
- Per user, per connection — the last 500 runs are kept; clear it anytime
Credential-bearing statements (IDENTIFIED BY, s3(...) keys) are redacted before storage, and history lives in CH-UI's local SQLite — it never leaves your server.
GET /api/query-history?search=&status=&limit=50&offset=0
DELETE /api/query-history/{id}
DELETE /api/query-history # clear all (current user + connection)Error Navigation
Failed queries show a structured error card instead of a raw dump: the ClickHouse error name and code (SYNTAX_ERROR · 62), a cleaned message, an actionable hint for common codes (unknown table/column, memory limit, timeouts…), and the full raw error in a collapsible.
When ClickHouse reports a position, a "Go to line N, col N" button jumps the editor cursor straight to the failing token and selects it — works with both pre- and post-25.2 ClickHouse error formats.
Schema Explorer
The left sidebar provides a navigable schema tree.
Explorer Endpoints
| Endpoint | Purpose |
|---|---|
GET /api/query/databases | List all databases |
GET /api/query/tables?database=db | List tables in a database |
GET /api/query/columns?database=db&table=tbl | List columns with types |
GET /api/query/data-types | List all ClickHouse data types |
GET /api/query/clusters | List cluster names |
Table Browser
Click a table in the explorer to open a table tab showing:
- Column list with types
- Data preview (paginated, default 100 rows, max 1000)
- Host info and table metadata
Autocomplete
The editor provides schema-aware completions using database, table, and column names from the explorer endpoints.
Schema Operations
Admin-only operations for managing databases and tables.
Create Database
POST /api/query/schema/database{
"name": "analytics",
"engine": "Atomic",
"on_cluster": "default",
"if_not_exists": true
}Drop Database
POST /api/query/schema/database/drop{
"name": "analytics",
"if_exists": true,
"sync": true
}Create Table
POST /api/query/schema/table{
"database": "analytics",
"name": "events",
"engine": "MergeTree",
"columns": [
{ "name": "id", "type": "UInt64" },
{ "name": "timestamp", "type": "DateTime" },
{ "name": "event_type", "type": "String" }
],
"order_by": "(timestamp)",
"partition_by": "toYYYYMM(timestamp)",
"if_not_exists": true
}Drop Table
POST /api/query/schema/table/drop{
"database": "analytics",
"name": "events",
"if_exists": true
}Data Upload
Upload CSV, JSON, JSONL, or Parquet files into ClickHouse tables. Max file size: 25 MB.
Two-Step Process
Step 1: Discover schema
POST /api/query/upload/discover
Content-Type: multipart/form-dataUpload the file. The backend detects the format, parses all rows, infers column types, and returns a preview (20 rows) with the inferred schema.
Type inference order: Bool > Int64 > Float64 > DateTime > Date > String. Nullable wrapper added if any null values found.
Step 2: Ingest data
POST /api/query/upload/ingest
Content-Type: multipart/form-dataConfirm the target database, table, and column mapping. Optionally create the table with a custom engine, ORDER BY, PARTITION BY, TTL, etc.
Data is inserted via JSONEachRow in 500-row batches with a 90-second timeout per batch.
Response
{
"success": true,
"database": "analytics",
"table": "events",
"rows_inserted": 15000,
"created_table": true,
"commands": {
"create_table": "CREATE TABLE ...",
"insert": "INSERT INTO ... FORMAT JSONEachRow"
}
}Saved Queries
Save, duplicate, update, and delete SQL snippets for reuse.
# List
GET /api/saved-queries
# Create
POST /api/saved-queries
{ "name": "Daily active users", "query": "SELECT count(DISTINCT user_id) ..." }
# Update (also used to rename — send just the name)
PUT /api/saved-queries/{id}
{ "name": "Updated name", "query": "..." }
# Duplicate (optionally pass a name for the copy)
POST /api/saved-queries/{id}/duplicate
{ "name": "My copy" } # optional; defaults to "<original> (copy)"
# Delete
DELETE /api/saved-queries/{id}Opening a saved query in the editor links the tab to the saved query ID. Changes are tracked via a dirty flag. Rename a saved query from the Saved Queries list (right-click → Rename) or by sending name to the PUT endpoint.
Query Parameters
Available on Pro and Enterprise plans.
Use ClickHouse's native bind-parameter syntax — {name:Type} — directly in your SQL. CH-UI detects parameters automatically and renders an input for each one above the results, so you can test parameterized queries in the editor without hitting an "unbound parameter" error.
SELECT *
FROM events
WHERE user_id = {user_id:UInt64}
AND created_at >= {since:DateTime}
LIMIT {limit:UInt32}Values are bound safely by ClickHouse (passed as param_<name> URL params), never string-interpolated into the SQL.
Default values
When you save a parameterized query, the current parameter values are stored as defaults (in the parameters field):
POST /api/saved-queries
{
"name": "Events for user",
"query": "SELECT * FROM events WHERE user_id = {user_id:UInt64}",
"parameters": { "user_id": "1001" }
}Running a saved query via the API
Execute a saved query — with parameters — and get the result back as JSON. Stored defaults are merged with any params you supply (your values win). Pro-only.
POST /api/saved-queries/{id}/run
{
"params": { "user_id": "42", "since": "2026-01-01 00:00:00", "limit": "100" },
"maxResultRows": 1000,
"timeout": 30
}The response matches the standard query-execution shape (data, meta, statistics, rows, elapsed_ms). On the free edition this endpoint returns 402 Payment Required.
Display Controls
Max Result Rows
Controls how many rows the UI requests from the server. Stored in localStorage (ch-ui-max-result-rows).
- Default: 1000
- Minimum: 1
- Sent as
maxResultRowsparameter on query execution
Number Formatting
Toggle locale-aware number formatting (thousands separators). Stored in localStorage (ch-ui-format-numbers), enabled by default.
Guardrails Integration
All query endpoints run governance guardrails before execution. If a policy with block enforcement mode matches, the query is rejected with a 403. See Governance for details.
Safety Defaults
| Control | Value |
|---|---|
| Query timeout | 30s default, 5 min max |
| Stream row limit | 1,000,000 |
| Upload file size | 25 MB |
| Upload batch size | 500 rows |
| Schema operations | Admin-only |
| Brain-generated SQL | LIMIT 100 default |