Skip to content
Closed
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,13 @@ 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
clickhousectl local client --host remote-host --version 26.8.1.1760 # Use an exact installed client binary
```

Named connections and direct connections select the client binary differently. A named connection uses the version recorded in the managed server's metadata, regardless of the global default. A direct connection (`--host`, `--port`, or both) uses the exact installed version passed with `--version`; if that flag is omitted, it uses the version selected by `local use`.

Direct connections never infer a binary from the contents of `~/.clickhouse/versions`. With no valid default, omitting `--version` fails whether zero, one, or multiple versions are installed. Use `local list` to find an exact installed version, then pass it with `--version` or select it globally with `local use`. A missing explicit version and a default that points to a removed version both fail with repair instructions. Direct `--version` selection neither installs a binary nor changes the default.

### Creating and managing ClickHouse servers

Start and manage ClickHouse server instances. Each server gets its own isolated data directory at `.clickhouse/servers/<name>/data/`.
Expand Down
15 changes: 15 additions & 0 deletions crates/clickhousectl/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ pub enum Error {
#[error("No default version set. Run: clickhousectl local use <version>")]
NoDefaultVersion,

#[error(
"No ClickHouse client version selected for the direct connection. Pass `--version <installed-version>`, or set a default with `clickhousectl local use <version>` (see `clickhousectl local list`)."
)]
DirectClientVersionRequired,

#[error(
"ClickHouse client version {0} is not installed. Run `clickhousectl local install {0}`, or choose an exact version from `clickhousectl local list`."
)]
ClientVersionNotInstalled(String),

#[error(
"Default ClickHouse version {0} is not installed. Repair it with `clickhousectl local use <version>`, or pass `--version <installed-version>` for this direct connection."
)]
StaleClientDefault(String),

#[error("Version {0} is already installed")]
VersionAlreadyInstalled(String),

Expand Down
93 changes: 88 additions & 5 deletions crates/clickhousectl/src/local/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ CONTEXT FOR AGENTS:
Init,

/// Connect to a running ClickHouse server with clickhouse-client
#[command(after_help = "\
#[command(
group(clap::ArgGroup::new("direct").args(["host", "port"]).multiple(true)),
after_help = "\
CONTEXT FOR AGENTS:
Two connection modes:
1. Named server: `clickhousectl local client --name dev` — looks up port and version from a
Expand All @@ -103,18 +105,28 @@ CONTEXT FOR AGENTS:
Named and direct selectors cannot be combined.
--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.")]
Related: `clickhousectl local server start` to start a local server, `clickhousectl local server list` to see servers."
)]
Client {
/// Server name to connect to (default: "default")
#[arg(long, short, conflicts_with_all = ["host", "port"])]
#[arg(long, short, conflicts_with_all = ["host", "port", "version"])]
name: Option<String>,

/// Exact installed ClickHouse version for a direct connection; does not change the default
#[arg(long, short = 'v', requires = "direct")]
version: Option<String>,

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

/// TCP port to connect to (bypasses local server lookup if set)
#[arg(long, short, value_parser = clap::value_parser!(u16).range(1..))]
#[arg(
long,
short,
value_parser = clap::value_parser!(u16).range(1..),
group = "direct"
)]
port: Option<u16>,

/// Execute a SQL query
Expand Down Expand Up @@ -683,6 +695,77 @@ mod tests {
}
}

#[test]
fn clickhouse_direct_client_version_selector_matrix() {
let cases = [
(
&["--host", "db.example", "--version", "25.12.9.61"][..],
"25.12.9.61",
),
(&["--port", "9000", "-v", "26.1.2.3"][..], "26.1.2.3"),
(
&[
"--version",
"26.2.3.4",
"--host",
"db.example",
"--port",
"9440",
][..],
"26.2.3.4",
),
];

for (selectors, expected_version) in cases {
let mut args = vec!["client"];
args.extend_from_slice(selectors);
let LocalCommands::Client { version, .. } = local_command(&args) else {
panic!("expected local client command");
};
assert_eq!(
version.as_deref(),
Some(expected_version),
"selectors: {selectors:?}"
);
}
}

#[test]
fn clickhouse_client_version_requires_direct_connection() {
for selectors in [
&["--version", "25.12.9.61"][..],
&["--name", "dev", "--version", "25.12.9.61"][..],
&["--version", "25.12.9.61", "--name", "dev"][..],
] {
let mut args = vec!["client"];
args.extend_from_slice(selectors);
let error = try_local_command(&args)
.err()
.expect("binary version should require direct mode");
assert!(
matches!(
error.kind(),
clap::error::ErrorKind::MissingRequiredArgument
| clap::error::ErrorKind::ArgumentConflict
),
"selectors: {selectors:?}\n{error}"
);
}
}

#[test]
fn clickhouse_client_help_describes_version_flag() {
let help = Cli::try_parse_from(["clickhousectl", "local", "client", "--help"])
.err()
.expect("help should exit through clap")
.to_string();

assert!(
help.contains("Exact installed ClickHouse version"),
"{help}"
);
}

#[test]
fn parses_remove_without_force() {
let LocalCommands::Remove { version, force } = local_command(&["remove", "25.12.5.44"])
Expand Down
24 changes: 22 additions & 2 deletions crates/clickhousectl/src/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ pub async fn run(cmd: LocalCommands, json: bool) -> Result<()> {
}
LocalCommands::Client {
name,
version,
host,
port,
query,
queries_file,
args,
} => run_client(name, host, port, query, queries_file, args),
} => run_client(name, version, host, port, query, queries_file, args),
LocalCommands::Server { command } => run_server_commands(command, json).await,
LocalCommands::Postgres { command } => postgres::run(command, json).await,
}
Expand Down Expand Up @@ -253,6 +254,7 @@ fn which(json: bool) -> Result<()> {

fn run_client(
name: Option<String>,
version: Option<String>,
host: Option<String>,
port: Option<u16>,
query: Option<String>,
Expand All @@ -264,7 +266,7 @@ fn run_client(
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()?;
let v = resolve_direct_client_version(version.as_deref())?;
(h, p, v)
} else {
let server_name = name.as_deref().unwrap_or("default");
Expand Down Expand Up @@ -313,6 +315,24 @@ fn run_client(
Err(Error::Exec(err.to_string()))
}

fn resolve_direct_client_version(version: Option<&str>) -> Result<String> {
if let Some(version) = version {
let installed = version_manager::list_installed_versions()?;
return installed
.iter()
.any(|installed| installed == version)
.then(|| version.to_string())
.ok_or_else(|| Error::ClientVersionNotInstalled(version.to_string()));
}

match version_manager::get_default_version() {
Ok(version) => Ok(version),
Err(Error::NoDefaultVersion) => Err(Error::DirectClientVersionRequired),
Err(Error::VersionNotFound(version)) => Err(Error::StaleClientDefault(version)),
Err(error) => Err(error),
}
}

#[allow(clippy::too_many_arguments)]
async fn start_server(
name: Option<String>,
Expand Down
Loading