Skip to content
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Cross-compilation for aarch64-linux uses `cross` (see `.github/workflows/release

`clickhousectl` is the official ClickHouse CLI — a version manager + cloud CLI. Two top-level subcommands: `local` and `cloud`.

1. **Local** (`local install|list|use|remove|which|init|run|server`) — version management in `src/version_manager/`, server management in `src/server.rs`, run/init in `main.rs`. Binaries live in `~/.clickhouse/versions/{version}/clickhouse`, default tracked in `~/.clickhouse/default`. Project data lives in `.clickhouse/`.
1. **Local** (`local install|list|use|remove|which|init|client|server`) — version management in `src/version_manager/`, server management in `src/server.rs`, client/init in `main.rs`. Binaries live in `~/.clickhouse/versions/{version}/clickhouse`, default tracked in `~/.clickhouse/default`. Project data lives in `.clickhouse/`.

2. **Cloud** (`cloud org|service|backup|auth`) — handled by `src/cloud/`. `CloudClient` wraps reqwest with Basic auth. Commands go through `cloud/commands.rs`, types in `cloud/types.rs`. All cloud commands support `--json` output.

Expand Down Expand Up @@ -62,7 +62,7 @@ cargo add rpassword # add latest version

- CLI is defined with clap derive macros in `src/cli.rs`, dispatched in `src/main.rs`
- `src/paths.rs` handles `~/.clickhouse/` paths (global install dir); `src/init.rs` handles `.clickhouse/` paths (project-local data dir)
- `run server` uses `exec()` (process replacement), so code after `cmd.exec()` only runs on failure
- `local client` uses `exec()` (process replacement), so code after `cmd.exec()` only runs on failure
- Error types use `thiserror` in `src/error.rs`; cloud module has its own error type wrapped as `Error::Cloud(String)`
- Version resolution (`version_manager/resolve.rs`) handles specs like `stable`, `lts`, `25.12`, or exact `25.12.5.44` — all resolve to an exact version + channel via GitHub API
- Releases are triggered by pushing a version tag (`v0.1.3`), which runs the GitHub Actions workflow
Expand All @@ -80,5 +80,5 @@ cargo add rpassword # add latest version
```bash
cargo run -- local install stable
cargo run -- local server start # starts server in .clickhouse/servers/default/
cargo run -- local run client -- --query "SELECT 1"
cargo run -- local client --query "SELECT 1"
```
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
With `clickhousectl` you can:
- Install and manage local ClickHouse versions
- Launch and manage local ClickHouse servers
- Execute queries against ClickHouse servers, or using clickhouse-local
- Execute queries against ClickHouse servers
- Setup ClickHouse Cloud and create cloud-managed ClickHouse clusters
- Manage ClickHouse Cloud resources
- Push your local ClickHouse development to cloud
Expand Down Expand Up @@ -90,8 +90,11 @@ clickhouse/

```bash
# Connect to a running server with clickhouse-client
clickhousectl local client
clickhousectl local client -- --host localhost --query "SHOW DATABASES"
clickhousectl local client # Connects to "default" server
clickhousectl local client --name dev # Connects to "dev" server
clickhousectl local client --query "SHOW DATABASES" # Run a query
clickhousectl local client --queries-file schema.sql # Run queries from a file
clickhousectl local client --host remote-host --port 9000 # Connect to a specific host/port
```

### Creating and managing ClickHouse servers
Expand Down
34 changes: 29 additions & 5 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,36 @@ CONTEXT FOR AGENTS:
/// Connect to a running ClickHouse server with clickhouse-client
#[command(after_help = "\
CONTEXT FOR AGENTS:
Connects to a running clickhouse-server. Server must already be running via `clickhousectl local server start`.
Pass clickhouse-client args after -- (e.g., `clickhousectl local client -- --query 'SELECT 1'`).
Common args: --host, --port, --query, --multiquery, --format.
Related: `clickhousectl local server start` to start a server first.")]
Two connection modes:
1. Named server: `clickhousectl local client --name dev` — looks up port and version from a
locally managed server started via `clickhousectl local server start`. Defaults to \"default\".
2. Explicit host/port: `clickhousectl local client --host myhost --port 9000` — connects to any
ClickHouse server directly, bypassing local server lookup.
--query and --queries-file execute SQL inline or from a file.
Additional clickhouse-client args can be passed after --.
Related: `clickhousectl local server start` to start a local server, `clickhousectl local server list` to see servers.")]
Client {
/// Arguments to pass to clickhouse-client
/// Server name to connect to (default: "default")
#[arg(long, short)]
name: Option<String>,

/// Host to connect to (bypasses local server lookup)
#[arg(long)]
host: Option<String>,

/// TCP port to connect to (bypasses local server lookup if set)
#[arg(long, short)]
port: Option<u16>,

/// Execute a SQL query
#[arg(long, short)]
query: Option<String>,

/// Execute queries from a SQL file
#[arg(long)]
queries_file: Option<String>,

/// Additional arguments to pass to clickhouse-client
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Expand Down
56 changes: 52 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@ async fn run_local(cmd: LocalCommands) -> Result<()> {
init::init()?;
Ok(())
}
LocalCommands::Client { args } => run_client(args),
LocalCommands::Client {
name,
host,
port,
query,
queries_file,
args,
} => run_client(name, host, port, query, queries_file, args),
LocalCommands::Server { command } => run_server_commands(command),
}
}
Expand Down Expand Up @@ -159,16 +166,57 @@ fn which() -> Result<()> {
Ok(())
}

fn run_client(args: Vec<String>) -> Result<()> {
let version = version_manager::get_default_version()?;
fn run_client(
name: Option<String>,
host: Option<String>,
port: Option<u16>,
query: Option<String>,
queries_file: Option<String>,
args: Vec<String>,
) -> Result<()> {
// If --host or --port is set, connect directly (bypass local server lookup).
// Otherwise, look up the named server for port and version.
let (resolved_host, tcp_port, version) = if host.is_some() || port.is_some() {
let h = host.unwrap_or_else(|| "localhost".to_string());
let p = port.unwrap_or(9000);
let v = version_manager::get_default_version()?;
(h, p, v)
} else {
let server_name = name.as_deref().unwrap_or("default");
let entries = server::list_all_servers();
let entry = entries
.iter()
.find(|e| e.name == server_name)
.ok_or_else(|| Error::ServerNotFound(server_name.to_string()))?;
let info = entry
.info
.as_ref()
.ok_or_else(|| Error::ServerNotRunning(server_name.to_string()))?;
("localhost".to_string(), info.tcp_port, info.version.clone())
};

let binary = paths::binary_path(&version)?;

if !binary.exists() {
return Err(Error::VersionNotFound(version));
}

let mut cmd = Command::new(&binary);
cmd.arg("client").args(&args);
cmd.arg("client")
.arg("--host")
.arg(&resolved_host)
.arg("--port")
.arg(tcp_port.to_string());

if let Some(q) = &query {
cmd.arg("--query").arg(q);
}

if let Some(f) = &queries_file {
cmd.arg("--queries-file").arg(f);
}

cmd.args(&args);
let err = cmd.exec();
Err(Error::Exec(err.to_string()))
}
Expand Down