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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,13 @@ clickhousectl local which # Show current default

# Remove a version
clickhousectl local remove 25.12.5.44
clickhousectl local remove 25.12.5.44 --force # Stop running servers on this version first
```

`local use` also creates a symlink at `~/.local/bin/clickhouse` pointing to the selected version's binary, so the plain `clickhouse` command (e.g. `clickhouse local`, `clickhouse client`) is on PATH. Pass `--no-global` to skip. If a regular file already exists at that path it is left alone with a warning. `local remove` of the active default version also clears the symlink.

`local remove` refuses to delete a version while a local server is running on it (it would leave the server pointing at a deleted binary), failing with the running server names. Stop the server first, or pass `--force` to stop the running server(s) and then remove the version.

#### ClickHouse binary storage

ClickHouse binaries are stored in a global repository, so they can be used by multiple projects without duplicating storage. Binaries are stored in `~/.clickhouse/`:
Expand Down
11 changes: 11 additions & 0 deletions crates/clickhousectl/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub enum Error {
#[error("Version {0} is already installed")]
VersionAlreadyInstalled(String),

#[error("Version {version} is in use by running server(s): {servers}. Stop them first, or pass --force.")]
VersionInUse { version: String, servers: String },

#[error("Unsupported platform: {os}/{arch}")]
UnsupportedPlatform { os: String, arch: String },

Expand Down Expand Up @@ -114,5 +117,13 @@ mod tests {
assert_eq!(Error::Cloud("boom".into()).exit_code(), 1);
assert_eq!(Error::NoVersionsInstalled.exit_code(), 1);
assert_eq!(Error::VersionNotFound("25.12".into()).exit_code(), 1);
assert_eq!(
Error::VersionInUse {
version: "25.12".into(),
servers: "default".into(),
}
.exit_code(),
1
);
}
}
27 changes: 27 additions & 0 deletions crates/clickhousectl/src/local/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,16 @@ CONTEXT FOR AGENTS:
Removes an installed ClickHouse version from ~/.clickhouse/versions/.
Takes an exact version string as shown by `clickhousectl local list` (e.g., \"25.12.5.44\").
Does NOT accept keywords like \"stable\" — use the exact version number.
Fails if a local server is currently running on this version; stop it first, or pass
--force to stop the running server(s) before removing.
Related: `clickhousectl local list` to see installed versions.")]
Remove {
/// Version to remove
version: String,

/// Stop any running servers using this version, then remove it
#[arg(long)]
force: bool,
},

/// Show the current default version
Expand Down Expand Up @@ -450,6 +456,27 @@ mod tests {
local.command
}

#[test]
fn parses_remove_without_force() {
let LocalCommands::Remove { version, force } = local_command(&["remove", "25.12.5.44"])
else {
panic!("expected remove");
};
assert_eq!(version, "25.12.5.44");
assert!(!force);
}

#[test]
fn parses_remove_with_force() {
let LocalCommands::Remove { version, force } =
local_command(&["remove", "25.12.5.44", "--force"])
else {
panic!("expected remove");
};
assert_eq!(version, "25.12.5.44");
assert!(force);
}

#[test]
fn parses_server_start_config_file() {
let LocalCommands::Server {
Expand Down
28 changes: 26 additions & 2 deletions crates/clickhousectl/src/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub async fn run(cmd: LocalCommands, json: bool) -> Result<()> {
}
}
LocalCommands::Use { version, no_global } => use_version(&version, no_global, json).await,
LocalCommands::Remove { version } => remove(&version, json),
LocalCommands::Remove { version, force } => remove(&version, force, json),
LocalCommands::Which => which(json),
LocalCommands::Init => {
init::init()?;
Expand Down Expand Up @@ -175,13 +175,37 @@ async fn use_version(version_spec: &str, no_global: bool, json: bool) -> Result<
Ok(())
}

fn remove(version: &str, json: bool) -> Result<()> {
fn remove(version: &str, force: bool, json: bool) -> Result<()> {
let version_dir = paths::version_dir(version)?;

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

// Recover orphaned servers so we detect a running process even when its
// metadata file is missing, then refuse to pull the binary out from under
// a server running on this version.
server::recover_current_project_servers();
let in_use: Vec<String> = server::list_running_servers()
.into_iter()
.filter(|i| i.version == version)
.map(|i| i.name)
.collect();
if !in_use.is_empty() {
if !force {
return Err(Error::VersionInUse {
version: version.to_string(),
servers: in_use.join(", "),
});
}
for name in &in_use {
server::kill_server(name)?;
if !json {
println!("Stopped server '{}'", name);
}
}
}

// Check if this is the default version
if let Ok(default) = version_manager::get_default_version()
&& default == version
Expand Down