Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions plugin/skills/hotdata/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ hotdata databases context push <name> [--database <database>] [--dry-run]
### Execute SQL Query

```
hotdata query "<sql>" [--workspace-id <workspace_id>] [--database <database>] [--dialect hotsql|duckdb|postgres|snowflake] [--output table|json|csv|id]
hotdata query "<sql>" [--workspace-id <workspace_id>] [--database <database>] [--dialect hotsql|duckdb|postgres|snowflake|clickhouse] [--output table|json|csv|id]
hotdata query [-d <database_id>] status <query_run_id> # -d before `status`, or set a current database first

# Same commands under the databases group (identical flags and exit codes)
Expand All @@ -269,7 +269,7 @@ hotdata databases query status <query_run_id>
- **`-o id`** prints only the query's stored `result_id` and downloads no rows — use it to feed `--result-id` loads: `RID=$(hotdata query "<sql>" -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 <id>` (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.
Expand Down
4 changes: 2 additions & 2 deletions plugin/skills/hotdata/subskills/analytics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ version: 0.45.1
## Execute SQL

```bash
hotdata query "<sql>" [--workspace-id <workspace_id>] [--database <database>] [--dialect hotsql|duckdb|postgres|snowflake] [--output table|json|csv]
hotdata query "<sql>" [--workspace-id <workspace_id>] [--database <database>] [--dialect hotsql|duckdb|postgres|snowflake|clickhouse] [--output table|json|csv]
hotdata query status <query_run_id>
```

- **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: `<catalog>.<schema>.<table>`, `<database>.<schema>.<table>`.
Expand Down
33 changes: 32 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -207,3 +207,34 @@ impl From<ShellChoice> 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"]));
}
}
2 changes: 1 addition & 1 deletion src/commands/databases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading