From ec547c017c2509c7b860ef230c5ff5781cb036b3 Mon Sep 17 00:00:00 2001 From: Eddie Tejeda <669988+eddietejeda@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:26:49 -0800 Subject: [PATCH] feat(query): accept --dialect clickhouse The server accepts ClickHouse SQL on the dialect flag (runtimedb#1759); the CLI checked the value against its own list first and refused it. Add it to `query` and `databases query`, and to the README and skill. --- README.md | 4 +-- plugin/skills/hotdata/SKILL.md | 4 +-- .../hotdata/subskills/analytics/SKILL.md | 4 +-- src/cli.rs | 33 ++++++++++++++++++- src/commands/databases.rs | 2 +- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 54e5ae9..f646f30 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ hotdata query "SELECT * FROM demo.public.trips LIMIT 10" ``` SQL is PostgreSQL-style. Add `-o json` or `-o csv` to change the output format. -If you prefer another dialect, pass `--dialect duckdb`, `postgres`, or -`snowflake` and the query is translated for you. +If you prefer another dialect, pass `--dialect duckdb`, `postgres`, +`snowflake`, or `clickhouse` and the query is translated for you. You can also write with SQL. `INSERT`, `CREATE TABLE`, `CREATE TABLE AS`, and `DROP TABLE` work on your current database's tables (your API key needs the diff --git a/plugin/skills/hotdata/SKILL.md b/plugin/skills/hotdata/SKILL.md index b9326f7..7385dd0 100644 --- a/plugin/skills/hotdata/SKILL.md +++ b/plugin/skills/hotdata/SKILL.md @@ -257,7 +257,7 @@ hotdata databases context push [--database ] [--dry-run] ### Execute SQL Query ``` -hotdata query "" [--workspace-id ] [--database ] [--dialect hotsql|duckdb|postgres|snowflake] [--output table|json|csv|id] +hotdata query "" [--workspace-id ] [--database ] [--dialect hotsql|duckdb|postgres|snowflake|clickhouse] [--output table|json|csv|id] hotdata query [-d ] status # -d before `status`, or set a current database first # Same commands under the databases group (identical flags and exit codes) @@ -269,7 +269,7 @@ hotdata databases query status - **`-o id`** prints only the query's stored `result_id` and downloads no rows — use it to feed `--result-id` loads: `RID=$(hotdata query "" -o id)`. Exits `1` if the query left no stored result. - **A query runs inside one instant database** (current database or `--database`); with none set it fails *"a database is required."* The scope sees the database's own catalog **plus whatever is attached to it only**. To read another database's tables or join across databases, attach it first — see [Querying across databases (attach)](#querying-across-databases-attach). - Use `hotdata databases tables list` and `hotdata databases tables show` for discovery — not `information_schema` via `query`. (Discovery lists every workspace table; queryability still requires the table's catalog to be in the current database's scope.) -- **PostgreSQL dialect.** Quote non-lowercase columns with double quotes. To write DuckDB/Postgres/Snowflake SQL instead, pass `--dialect` (server-side transpile; writes follow the same rules as `hotsql`) — details in **`hotdata-analytics`**. +- **PostgreSQL dialect.** Quote non-lowercase columns with double quotes. To write DuckDB/Postgres/Snowflake/ClickHouse SQL instead, pass `--dialect` (server-side transpile; writes follow the same rules as `hotsql`) — details in **`hotdata-analytics`**. - **SQL writes.** `query` runs `INSERT INTO t [(cols)] VALUES …|SELECT …`, `UPDATE t SET col = expr, … [WHERE …]`, `DELETE FROM t [WHERE …]`, `CREATE TABLE t (col TYPE, …)`, `CREATE TABLE t AS SELECT …`, and `DROP TABLE [IF EXISTS] t` against the **current database's own tables**; attached databases stay read-only. The API key needs the `write` permission. The result is one row, `rows_affected` (for `UPDATE`, the rows its `WHERE` matched). `UPDATE`/`DELETE` match rows by their internal rowid, so the table **needs no key**, and subqueries in `WHERE` work. Each write is a full publish (write lock, new generation, index maintenance), so **batch rows into one statement** and use `hotdata databases load` for anything of size. For bulk changes by key, `hotdata databases load --mode update|delete|upsert` is still the better tool. Refused: a target alias, `DELETE … LIMIT`/`USING`/`ORDER BY`, `UPDATE … FROM`/joins, `SET t.col` or the same column twice, `SET` of a key or `--key-determines` column on a table that declares `--key-determines`, `INSERT OVERWRITE`/`OR REPLACE`/`ON CONFLICT`/`RETURNING`, column constraints on `CREATE TABLE` (`NOT NULL`, `DEFAULT`, `PRIMARY KEY`, `UNIQUE` — every column is nullable), `ALTER`, views, `COPY`, transactions. A table with a vector index must be inserted into with a column list that omits the generated embedding column. Table layout (`--key`, partition, sort, `--layout`) is declared with `databases tables add`, not in SQL. - Async runs return `query_run_id` → poll with `query status ` (do not re-run the same heavy SQL). `query status` exit codes: `0` succeeded, `1` failed, `2` still running (poll again), `3` succeeded but the result is a truncated/incomplete preview. - **Large results: `-o csv` / `-o json` are complete, `-o table` is capped.** The server returns inline rows only up to a bounded cap and persists the full set under a `result_id`. For `csv` and `json` the CLI **streams** that full result batch by batch, so output size is unbounded and memory stays flat — pipe a big result to a file with `-o csv`/`-o json`, never `table`. For `table` the CLI fetches at most **10,000 rows** and the footer says `N of TOTAL rows — INCOMPLETE PREVIEW` (with `?` when the server sent no total); the process exits **3** so a pipeline cannot mistake it for the whole set. Same rules for `hotdata databases results get`. A result the server is still writing is waited for (up to 5 minutes, honoring `Retry-After`) rather than returned partial. If the full result can't be retrieved, the CLI prints the inline preview, a `warning:` to stderr, and exits 3. `-o json` carries `row_count`, `total_row_count`, and `truncated` — branch on `truncated`, not on the row count. diff --git a/plugin/skills/hotdata/subskills/analytics/SKILL.md b/plugin/skills/hotdata/subskills/analytics/SKILL.md index 1541c62..1515e00 100644 --- a/plugin/skills/hotdata/subskills/analytics/SKILL.md +++ b/plugin/skills/hotdata/subskills/analytics/SKILL.md @@ -17,12 +17,12 @@ version: 0.45.1 ## Execute SQL ```bash -hotdata query "" [--workspace-id ] [--database ] [--dialect hotsql|duckdb|postgres|snowflake] [--output table|json|csv] +hotdata query "" [--workspace-id ] [--database ] [--dialect hotsql|duckdb|postgres|snowflake|clickhouse] [--output table|json|csv] hotdata query status ``` - **PostgreSQL dialect.** Quote mixed-case identifiers: `"CustomerName"`. -- **`--dialect`** (default `hotsql`): write SQL in `duckdb`/`postgres`/`snowflake` and the server transpiles it to HotSQL before running (e.g. Snowflake `IFF(...)`, DuckDB `len(...)`). Writes follow the same rules as `hotsql`. +- **`--dialect`** (default `hotsql`): write SQL in `duckdb`/`postgres`/`snowflake`/`clickhouse` and the server transpiles it to HotSQL before running (e.g. Snowflake `IFF(...)`, DuckDB `len(...)`, ClickHouse `countIf(...)`). Writes follow the same rules as `hotsql`. - **Writes:** `INSERT` (append only), `CREATE TABLE`, `CREATE TABLE AS SELECT`, and `DROP TABLE` run through `query` against the current database's own tables and return one row, `rows_affected`; `UPDATE`/`DELETE` are not supported in SQL yet (use keyed `hotdata databases load --mode update|delete|upsert`). Rules and refusals in the **`hotdata`** skill → Execute SQL Query. - Use **`hotdata databases tables list`** for schema discovery — not `information_schema` via `query`. - Fully qualified names: `..`, `..
`. diff --git a/src/cli.rs b/src/cli.rs index 96801e5..62b933d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -59,7 +59,7 @@ pub enum Commands { /// SQL dialect the query is written in — a non-`hotsql` dialect is /// transpiled to HotSQL server-side before it runs (writes follow the same /// rules as hotsql) - #[arg(long, default_value = "hotsql", value_parser = ["hotsql", "duckdb", "postgres", "snowflake"])] + #[arg(long, default_value = "hotsql", value_parser = ["hotsql", "duckdb", "postgres", "snowflake", "clickhouse"])] dialect: String, /// Output format. `id` prints only the stored result's id, without @@ -207,3 +207,34 @@ impl From for clap_complete::Shell { } } } + +#[cfg(test)] +mod tests { + use super::Commands; + use clap::Parser; + + #[derive(Parser)] + struct Wrapper { + #[command(subcommand)] + cmd: Commands, + } + + fn parses(args: &[&str]) -> bool { + Wrapper::try_parse_from(std::iter::once("t").chain(args.iter().copied())).is_ok() + } + + #[test] + fn query_accepts_each_server_dialect() { + for dialect in ["hotsql", "duckdb", "postgres", "snowflake", "clickhouse"] { + assert!( + parses(&["query", "SELECT 1", "--dialect", dialect]), + "query --dialect {dialect}" + ); + assert!( + parses(&["databases", "query", "SELECT 1", "--dialect", dialect]), + "databases query --dialect {dialect}" + ); + } + assert!(!parses(&["query", "SELECT 1", "--dialect", "mysql"])); + } +} diff --git a/src/commands/databases.rs b/src/commands/databases.rs index c1d348b..87f251e 100644 --- a/src/commands/databases.rs +++ b/src/commands/databases.rs @@ -305,7 +305,7 @@ pub enum DatabasesCommands { /// SQL dialect the query is written in — a non-`hotsql` dialect is /// transpiled to HotSQL server-side before it runs (writes follow the same /// rules as hotsql) - #[arg(long, default_value = "hotsql", value_parser = ["hotsql", "duckdb", "postgres", "snowflake"])] + #[arg(long, default_value = "hotsql", value_parser = ["hotsql", "duckdb", "postgres", "snowflake", "clickhouse"])] dialect: String, /// Output format. `id` prints only the stored result's id, without