use crate::cli::DatabricksCli;
use crate::fetchers::preview::run_sql;
use crate::shape::TableData;
const BUCKET_CASE: &str = "CASE \
WHEN u.sku_name LIKE '%JOBS%' THEN 'Jobs' \
WHEN u.sku_name LIKE '%DLT%' THEN 'DLT' \
WHEN u.sku_name LIKE '%SQL%' THEN 'SQL' \
WHEN u.sku_name LIKE '%ALL_PURPOSE%' THEN 'All-Purpose' \
ELSE 'Other' END";
#[derive(Debug, Clone)]
pub struct CostDay {
pub date: String,
pub by_bucket: Vec<(String, f64)>,
pub total: f64,
pub total_usd: f64,
}
#[derive(Debug, Clone)]
pub struct Spender {
pub kind: String,
pub id: String,
pub dbus: f64,
pub usd: f64,
}
#[derive(Debug, Clone)]
pub struct CostData {
pub days: Vec<CostDay>,
pub buckets: Vec<(String, f64, f64)>,
pub total: f64,
pub total_usd: f64,
pub priced: bool,
pub spenders: Vec<Spender>,
pub scoped: bool,
}
fn ws_clause(workspace_id: Option<&str>) -> String {
match workspace_id {
Some(id) => format!(" AND u.workspace_id = '{}'", id.replace('\'', "")),
None => String::new(),
}
}
fn priced_query(ws: &str) -> String {
format!(
"SELECT u.usage_date, {BUCKET_CASE} AS bucket, \
ROUND(SUM(u.usage_quantity), 2) AS dbus, \
ROUND(SUM(u.usage_quantity * COALESCE(lp.pricing.default, 0)), 2) AS usd \
FROM system.billing.usage u \
LEFT JOIN system.billing.list_prices lp \
ON u.sku_name = lp.sku_name AND u.usage_unit = lp.usage_unit \
AND u.usage_end_time >= lp.price_start_time \
AND (lp.price_end_time IS NULL OR u.usage_end_time < lp.price_end_time) \
WHERE u.usage_date >= date_sub(current_date(), 13){ws} \
GROUP BY 1, 2 ORDER BY 1"
)
}
const SPENDER_KIND: &str = "CASE \
WHEN u.usage_metadata.job_id IS NOT NULL THEN 'job' \
WHEN u.usage_metadata.warehouse_id IS NOT NULL THEN 'warehouse' \
WHEN u.usage_metadata.cluster_id IS NOT NULL THEN 'cluster' \
ELSE 'other' END";
const SPENDER_ID: &str = "COALESCE(u.usage_metadata.job_id, \
u.usage_metadata.warehouse_id, u.usage_metadata.cluster_id, u.sku_name)";
fn spenders_query(priced: bool, ws: &str) -> String {
let usd = if priced {
", ROUND(SUM(u.usage_quantity * COALESCE(lp.pricing.default, 0)), 2) AS usd"
} else {
""
};
let join = if priced {
"LEFT JOIN system.billing.list_prices lp \
ON u.sku_name = lp.sku_name AND u.usage_unit = lp.usage_unit \
AND u.usage_end_time >= lp.price_start_time \
AND (lp.price_end_time IS NULL OR u.usage_end_time < lp.price_end_time) "
} else {
""
};
let order = if priced { "4" } else { "3" };
format!(
"SELECT {SPENDER_KIND} AS kind, {SPENDER_ID} AS id, \
ROUND(SUM(u.usage_quantity), 2) AS dbus{usd} \
FROM system.billing.usage u {join}\
WHERE u.usage_date >= date_sub(current_date(), 13){ws} \
GROUP BY 1, 2 ORDER BY {order} DESC LIMIT 10"
)
}
fn parse_spenders(table: &TableData) -> Vec<Spender> {
table
.rows
.iter()
.filter_map(|row| {
let (kind, id, dbus, usd) = match row.as_slice() {
[k, i, d, u] => (k, i, d.parse().ok()?, u.parse().unwrap_or(0.0)),
[k, i, d] => (k, i, d.parse().ok()?, 0.0),
_ => return None,
};
Some(Spender {
kind: kind.clone(),
id: id.clone(),
dbus,
usd,
})
})
.collect()
}
fn plain_query(ws: &str) -> String {
format!(
"SELECT u.usage_date, {BUCKET_CASE} AS bucket, \
ROUND(SUM(u.usage_quantity), 2) AS dbus \
FROM system.billing.usage u \
WHERE u.usage_date >= date_sub(current_date(), 13){ws} \
GROUP BY 1, 2 ORDER BY 1"
)
}
fn aggregate(table: &TableData, priced: bool) -> CostData {
let mut days: Vec<CostDay> = Vec::new();
let mut bucket_totals: Vec<(String, f64, f64)> = Vec::new();
for row in &table.rows {
let (date, bucket, dbus, usd) = match row.as_slice() {
[d, b, v, u] => (d, b, v.parse().unwrap_or(0.0), u.parse().unwrap_or(0.0)),
[d, b, v] => (d, b, v.parse().unwrap_or(0.0), 0.0),
_ => continue,
};
if days.last().map(|d| &d.date) != Some(date) {
days.push(CostDay {
date: date.clone(),
by_bucket: Vec::new(),
total: 0.0,
total_usd: 0.0,
});
}
let day = days.last_mut().unwrap();
day.by_bucket.push((bucket.clone(), dbus));
day.total += dbus;
day.total_usd += usd;
match bucket_totals.iter_mut().find(|(b, _, _)| b == bucket) {
Some((_, t, tu)) => {
*t += dbus;
*tu += usd;
}
None => bucket_totals.push((bucket.clone(), dbus, usd)),
}
}
bucket_totals.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let total = bucket_totals.iter().map(|(_, t, _)| t).sum();
let total_usd = bucket_totals.iter().map(|(_, _, u)| u).sum();
CostData {
days,
buckets: bucket_totals,
total,
total_usd,
priced,
spenders: Vec::new(),
scoped: false,
}
}
pub async fn resolve_workspace_id(
cli: &DatabricksCli,
warehouse_id: &str,
host: &str,
) -> Option<String> {
let hostname = host
.trim_start_matches("https://")
.trim_start_matches("http://")
.trim_end_matches('/')
.replace('\'', "");
let sql = format!(
"SELECT CAST(workspace_id AS STRING) \
FROM system.access.workspaces_latest \
WHERE workspace_url LIKE '%{hostname}%' LIMIT 2"
);
let table = run_sql(cli, &sql, warehouse_id).await.ok()?;
match table.rows.as_slice() {
[row] => row.first().cloned(),
_ => None,
}
}
pub async fn fetch(
cli: &DatabricksCli,
warehouse_id: &str,
workspace_id: Option<&str>,
) -> Result<CostData, String> {
let ws = ws_clause(workspace_id);
let mut data = match run_sql(cli, &priced_query(&ws), warehouse_id).await {
Ok(table) => aggregate(&table, true),
Err(_) => {
let table = run_sql(cli, &plain_query(&ws), warehouse_id).await?;
aggregate(&table, false)
}
};
data.scoped = workspace_id.is_some();
if let Ok(table) = run_sql(cli, &spenders_query(data.priced, &ws), warehouse_id).await {
data.spenders = parse_spenders(&table);
if data.priced {
data.spenders.sort_by(|a, b| {
b.usd
.partial_cmp(&a.usd)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
}
Ok(data)
}