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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ target
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

AGENTS.md
2 changes: 1 addition & 1 deletion src/config_loader/process_detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ pub fn get_paths(env: Environment) -> Result<(PathBuf, PathBuf)> {
let (install_path, jdk_path) = get_paths_by_pid(pid);

// Verify that we have a valid DORIS_HOME path
if install_path == PathBuf::from("/opt/selectdb") {
if install_path.as_path() == Path::new("/opt/selectdb") {
return Err(CliError::ConfigError(format!(
"DORIS_HOME not found in {env} process environment"
)));
Expand Down
127 changes: 112 additions & 15 deletions src/tools/mysql/client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::config_loader::Environment;
use crate::config_loader::process_detector;
use crate::error::{CliError, Result};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;

pub struct MySQLTool;
Expand All @@ -14,6 +17,88 @@ enum OutputMode {
Raw,
}

struct TempMySqlDefaultsFile {
path: PathBuf,
}

impl TempMySqlDefaultsFile {
fn path(&self) -> &Path {
&self.path
}

fn create(host: &str, port: u16, user: &str, password: &str) -> Result<Self> {
let random: [u8; 16] = rand::random();
let suffix = random
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>();
let filename = format!("cloud-cli-mysql-{suffix}.cnf");
let path = std::env::temp_dir().join(filename);

let mut open_options = fs::OpenOptions::new();
open_options.write(true).create_new(true);

#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
open_options.mode(0o600);
}

let mut file = open_options.open(&path).map_err(|e| {
CliError::ToolExecutionFailed(format!(
"Failed to create mysql defaults file at {}: {e}",
path.display()
))
})?;

let user = escape_mysql_option_value(user);
let host = escape_mysql_option_value(host);
let password = escape_mysql_option_value(password);

writeln!(file, "[client]").map_err(CliError::IoError)?;
writeln!(file, "user=\"{user}\"").map_err(CliError::IoError)?;
writeln!(file, "host=\"{host}\"").map_err(CliError::IoError)?;
writeln!(file, "port={port}").map_err(CliError::IoError)?;
writeln!(file, "protocol=tcp").map_err(CliError::IoError)?;
if !password.is_empty() {
writeln!(file, "password=\"{password}\"").map_err(CliError::IoError)?;
}

#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&path)
.map_err(CliError::IoError)?
.permissions();
perms.set_mode(0o600);
fs::set_permissions(&path, perms).map_err(CliError::IoError)?;
}

Ok(Self { path })
}
}

impl Drop for TempMySqlDefaultsFile {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}

fn escape_mysql_option_value(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for ch in input.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(ch),
}
}
out
}

impl MySQLTool {
pub fn detect_fe_process() -> Result<u32> {
process_detector::get_pid_by_env(Environment::FE)
Expand Down Expand Up @@ -110,35 +195,47 @@ impl MySQLTool {
query: &str,
mode: OutputMode,
) -> Result<std::process::Output> {
let mut command = Command::new("mysql");
command.arg("-h").arg(host);
command.arg("-P").arg(port.to_string());
command.arg("-u").arg(user);
let (mut command, _defaults_file) =
Self::build_mysql_command(host, port, user, password, query, mode)?;

if !password.is_empty() {
command.arg(format!("-p{password}"));
}
command
.output()
.map_err(|e| CliError::ToolExecutionFailed(format!("Failed to execute mysql: {e}")))
}

fn build_mysql_command(
host: &str,
port: u16,
user: &str,
password: &str,
query: &str,
mode: OutputMode,
) -> Result<(Command, TempMySqlDefaultsFile)> {
let defaults_file = TempMySqlDefaultsFile::create(host, port, user, password)?;

let mut command = Command::new("mysql");
// Must be the first argument to ensure MySQL reads only this file and does not
// merge/override with system/user option files such as ~/.my.cnf.
command.arg(format!(
"--defaults-file={}",
defaults_file.path().display()
));

match mode {
OutputMode::Standard => {
command.arg("-A");
}
OutputMode::Raw => {
command.arg("-N");
command.arg("-B");
command.arg("-r");
command.arg("-A");
command.args(["-N", "-B", "-r", "-A"]);
}
}

command.arg("-e").arg(query);

// Prevent mysql from prompting for a password interactively
// Prevent mysql from prompting for a password interactively.
command.stdin(std::process::Stdio::null());

command
.output()
.map_err(|e| CliError::ToolExecutionFailed(format!("Failed to execute mysql: {e}")))
Ok((command, defaults_file))
}

/// Lists databases (excluding system databases) using raw mysql output
Expand Down