use crate::model::{OutputFormat, QueriesSortBy};
use clap::{ArgGroup, Args, Parser, Subcommand};
use std::path::PathBuf;
use std::str::FromStr;
use time::format_description::well_known::Rfc3339;
use time::macros::format_description;
use time::{Date, OffsetDateTime, Time};
#[derive(Parser)]
#[command(
name = "clickcheck",
version,
about = "Tool to analyze ClickHouse system tables, to detect potential issues for DBAs."
)]
pub struct CliArgs {
#[command(subcommand)]
pub command: Command,
#[arg(long, global = true)]
pub config: Option<PathBuf>,
#[arg(long, global = true)]
pub context: Option<String>,
#[clap(long, global = true, default_value = "text")]
pub out: OutputFormat,
}
#[derive(Subcommand)]
pub enum Command {
Queries {
#[clap(flatten)]
conn: ConnectArgs,
#[arg(long, default_value = "total-impact")]
sort_by: QueriesSortBy,
#[clap(flatten)]
filter: QueriesFilterArgs,
#[arg(long, default_value_t = 5)]
limit: usize,
},
Total {
#[clap(flatten)]
conn: ConnectArgs,
#[clap(flatten)]
filter: QueriesFilterArgs,
},
Inspect {
#[clap(flatten)]
conn: ConnectArgs,
#[arg(value_parser = parse_hex)]
fingerprint: u64,
#[clap(flatten)]
filter: QueriesFilterArgs,
},
Errors {
#[clap(flatten)]
conn: ConnectArgs,
#[clap(flatten)]
filter: ErrorFilterArgs,
#[arg(long, default_value_t = 5)]
limit: usize,
},
Context {
#[command(subcommand)]
command: ContextCommand,
},
}
#[derive(Args, Clone, Debug)]
pub struct ConnectArgs {
#[arg(short = 'U', long = "url")]
pub urls: Vec<String>,
#[arg(short = 'u', long)]
pub user: Option<String>,
#[arg(short = 'p', long, value_parser = parse_secret_arg)]
pub password: Option<secrecy::SecretString>,
#[arg(short = 'i', long, conflicts_with = "password")]
pub interactive_password: bool,
#[arg(long)]
pub accept_invalid_certificate: Option<bool>,
}
#[derive(Args, Clone)]
#[command(group(
ArgGroup::new("from_or_last")
.args(["from", "last"])
.required(true)
))]
pub struct QueriesFilterArgs {
#[arg(
long,
value_parser = parse_datetime,
group = "from_or_last"
)]
pub from: Option<OffsetDateTime>,
#[arg(long, value_parser = parse_datetime)]
pub to: Option<OffsetDateTime>,
#[arg(
long,
value_parser = humantime::parse_duration,
group = "from_or_last"
)]
pub last: Option<std::time::Duration>,
#[arg(long = "query-user")]
pub query_user: Vec<String>,
#[arg(long)]
pub database: Vec<String>,
#[arg(long)]
pub table: Vec<String>,
#[arg(long, value_parser = humantime::parse_duration)]
pub min_query_duration: Option<std::time::Duration>,
#[arg(long)]
pub min_read_rows: Option<u64>,
#[arg(long, value_parser = bytesize::ByteSize::from_str)]
pub min_read_data: Option<bytesize::ByteSize>,
}
#[derive(Args, Debug, Clone)]
pub struct ErrorFilterArgs {
#[arg(long, value_parser = humantime::parse_duration)]
pub last: Option<std::time::Duration>,
#[arg(long)]
pub min_count: Option<usize>,
#[arg(long)]
pub code: Vec<i32>,
}
#[derive(Subcommand)]
pub enum ContextCommand {
ConfigPath,
List,
Current,
Show {
name: String,
#[arg(long, default_value = "false")]
show_secrets: bool,
},
Set {
#[command(subcommand)]
command: ContextSetCommand,
},
Delete { name: String },
}
#[derive(Subcommand)]
pub enum ContextSetCommand {
Profile(SetProfileArgs),
Current { name: String },
}
#[derive(Args)]
#[command(group( ArgGroup::new("auth") .args(["password", "interactive_password"]) .required(true)))]
pub struct SetProfileArgs {
pub name: String,
#[arg(short = 'U', long = "url", required = true)]
pub urls: Vec<String>,
#[arg(short = 'u', long, required = true)]
pub user: String,
#[arg( short = 'p', long, value_parser = parse_secret_arg, group = "auth")]
pub password: Option<secrecy::SecretString>,
#[arg(short = 'i', long, group = "auth")]
pub interactive_password: bool,
#[arg(long, default_value_t = false)]
pub accept_invalid_certificate: bool,
}
fn parse_datetime(s: &str) -> Result<OffsetDateTime, String> {
if let Ok(dt) = OffsetDateTime::parse(s, &Rfc3339) {
return Ok(dt);
}
let date_format = format_description!("[year]-[month]-[day]");
if let Ok(date) = Date::parse(s, &date_format) {
let date = date.with_time(Time::MIDNIGHT).assume_utc();
return Ok(date);
}
Err("Invalid datetime format. Use RFC3339 (e.g. 2024-05-01T10:30:00Z) or YYYY-MM-DD.".into())
}
fn parse_secret_arg(s: &str) -> Result<secrecy::SecretString, String> {
Ok(secrecy::SecretString::new(s.to_string().into()))
}
fn parse_hex(s: &str) -> Result<u64, String> {
u64::from_str_radix(s.trim_start_matches("0x"), 16)
.map_err(|e| format!("Invalid hex value: {}", e))
}