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: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ postgres/
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 --query "SELECT 1" --query "SELECT 2" # Run queries in order
clickhousectl local client --queries-file schema.sql # Run queries from a file
clickhousectl local client --queries-file schema.sql seed.sql # Run files in order
clickhousectl local client --host remote-host --port 9000 # Connect to a specific host/port
clickhousectl local client --host remote-host # Direct mode; port defaults to 9000
clickhousectl local client --port 19000 # Direct mode; host defaults to localhost
Expand All @@ -235,6 +237,8 @@ In direct mode, `--host` and `--port` select the server connection while `--vers

Without `--version`, direct mode uses the valid default. If no default exists, zero installed versions is an error, one installed version is used without creating a default, and multiple installed versions require either `--version` or `local use`. A default that names a missing binary is an error; repair it with `local use`, or bypass it for one direct connection with `--version`.

`--query` can be repeated, while each `--queries-file` accepts one or more paths and the flag itself can also be repeated. Values, including empty strings, are passed to the native client unchanged and in order. The two options cannot be combined because the native ClickHouse client rejects that combination, so clickhousectl reports a usage error before resolving a binary. Arguments after `--` are appended after all wrapper-generated arguments. Repeatable `--query` requires ClickHouse 23.9.1.1854 or newer, where [ClickHouse added the native behavior](https://github.com/ClickHouse/ClickHouse/blob/8f9a227de1f530cdbda52c145d41a6b0f1d29961/docs/changelogs/archive/v23.9.1.1854-stable.md); clickhousectl checks the selected client version before execution.

### 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
8 changes: 8 additions & 0 deletions crates/clickhousectl/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ pub enum Error {
)]
ClientVersionNotInstalled(String),

#[error(
"ClickHouse client version '{version}' does not support repeated --query values. Use ClickHouse {minimum} or newer, or send one --query value."
)]
RepeatedClientQueryUnsupported {
version: String,
minimum: &'static str,
},

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

Expand Down
107 changes: 100 additions & 7 deletions crates/clickhousectl/src/local/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,9 @@ CONTEXT FOR AGENTS:
2. Explicit host/port: `clickhousectl local client --host myhost --port 9000` — connects to any
ClickHouse server directly, bypassing local server lookup. Host-only uses port 9000; port-only
connects to localhost. Direct selectors cannot be combined with --name.
--query and --queries-file execute SQL inline or from a file.
Repeat --query to execute multiple inline queries. --queries-file accepts multiple paths after
one flag and can also be repeated. ClickHouse rejects combining the two options, so clickhousectl
reports that combination as a usage error.
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."
)]
Expand All @@ -274,13 +276,13 @@ CONTEXT FOR AGENTS:
#[arg(long, short = 'v', requires = "direct", conflicts_with = "name")]
version: Option<ClientVersionArg>,

/// Execute a SQL query
#[arg(long, short)]
query: Option<String>,
/// Execute a SQL query; repeat for multiple queries (requires ClickHouse 23.9.1.1854+)
#[arg(long, short, conflicts_with = "queries_file")]
query: Vec<String>,

/// Execute queries from a SQL file
#[arg(long)]
queries_file: Option<String>,
/// Execute queries from SQL files; accepts multiple paths or repeated flags
#[arg(long, num_args = 1.., conflicts_with = "query")]
queries_file: Vec<String>,

/// Additional arguments to pass to clickhouse-client
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
Expand Down Expand Up @@ -940,6 +942,82 @@ mod tests {
}
}

#[test]
fn clickhouse_client_parses_minimal_and_repeated_query_inputs_in_order() {
let LocalCommands::Client {
query,
queries_file,
args,
..
} = local_command(&["client"])
else {
panic!("expected ClickHouse client");
};
assert!(query.is_empty());
assert!(queries_file.is_empty());
assert!(args.is_empty());

let LocalCommands::Client {
query,
queries_file,
args,
..
} = local_command(&[
"client", "--query", "SELECT 1", "-q", "SELECT 2", "--query", "", "--", "--query",
"SELECT 3", "--format", "CSV",
])
else {
panic!("expected ClickHouse client");
};
assert_eq!(query, ["SELECT 1", "SELECT 2", ""]);
assert!(queries_file.is_empty());
assert_eq!(args, ["--query", "SELECT 3", "--format", "CSV"]);
}

#[test]
fn clickhouse_client_parses_repeated_query_files_and_empty_values_in_order() {
let LocalCommands::Client {
query,
queries_file,
args,
..
} = local_command(&[
"client",
"--queries-file",
"schema.sql",
"seed.sql",
"--queries-file",
"",
"verify.sql",
"--",
"--queries-file",
"tail.sql",
])
else {
panic!("expected ClickHouse client");
};
assert!(query.is_empty());
assert_eq!(queries_file, ["schema.sql", "seed.sql", "", "verify.sql"]);
assert_eq!(args, ["--queries-file", "tail.sql"]);
}

#[test]
fn clickhouse_client_rejects_combined_query_sources_in_every_order() {
for inputs in [
["--query", "SELECT 1", "--queries-file", "queries.sql"],
["--queries-file", "queries.sql", "--query", "SELECT 1"],
] {
let args: Vec<&str> = ["client"].into_iter().chain(inputs).collect();
let error = local_parse_error(&args);
assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict);
assert!(error.to_string().contains("--query <QUERY>"), "{error}");
assert!(
error.to_string().contains("--queries-file <QUERIES_FILE>"),
"{error}"
);
}
}

#[test]
fn clickhouse_client_preserves_passthrough_selector_like_arguments() {
let LocalCommands::Client { name, args, .. } = local_command(&[
Expand Down Expand Up @@ -987,6 +1065,21 @@ mod tests {
}
}

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

assert!(help.contains("repeat for multiple queries"), "{help}");
assert!(
help.contains("accepts multiple paths or repeated flags"),
"{help}"
);
assert!(help.contains("ClickHouse rejects combining"), "{help}");
}

#[test]
fn postgres_client_applies_named_and_direct_selector_validation() {
let valid: &[&[&str]] = &[
Expand Down
36 changes: 32 additions & 4 deletions crates/clickhousectl/src/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ fn run_client(
host: Option<String>,
port: Option<u16>,
version_spec: Option<ClientVersionArg>,
query: Option<String>,
queries_file: Option<String>,
query: Vec<String>,
queries_file: Vec<String>,
args: Vec<String>,
) -> Result<()> {
// If --host or --port is set, connect directly (bypass local server lookup).
Expand Down Expand Up @@ -295,18 +295,20 @@ fn run_client(
return Err(Error::VersionNotFound(version));
}

ensure_repeated_query_supported(&version, query.len())?;

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

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

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

Expand All @@ -319,6 +321,21 @@ fn run_client(
Err(Error::Exec(err.to_string()))
}

const REPEATED_QUERY_MIN_VERSION: &str = "23.9.1.1854";

fn ensure_repeated_query_supported(version: &str, query_count: usize) -> Result<()> {
if query_count > 1
&& version_manager::list::compare_versions(version, REPEATED_QUERY_MIN_VERSION)
== std::cmp::Ordering::Less
{
return Err(Error::RepeatedClientQueryUnsupported {
version: version.to_string(),
minimum: REPEATED_QUERY_MIN_VERSION,
});
}
Ok(())
}

fn resolve_direct_client_version(version_spec: Option<ClientVersionArg>) -> Result<String> {
if let Some(version_spec) = version_spec {
let spec = version_spec.into_spec();
Expand Down Expand Up @@ -1104,6 +1121,17 @@ fn stop_all_servers_global(json: bool) -> Result<()> {
mod tests {
use super::*;

#[test]
fn repeated_query_support_matches_the_native_client_contract() {
// ClickHouse introduced repeatable --query in v23.9.1.1854. The pinned
// release evidence is linked from README.md; these tests need no live binary.
assert!(ensure_repeated_query_supported("23.8.1.2992", 1).is_ok());
assert!(ensure_repeated_query_supported("23.9.1.1853", 2).is_err());
assert!(ensure_repeated_query_supported(REPEATED_QUERY_MIN_VERSION, 2).is_ok());
assert!(ensure_repeated_query_supported("25.12.9.61", 3).is_ok());
assert!(ensure_repeated_query_supported("26.8.1.1760", usize::MAX).is_ok());
}

fn server_info(name: &str, engine: server::Engine, version: &str) -> server::ServerInfo {
server::ServerInfo {
name: name.to_string(),
Expand Down
Loading