use super::select_ast::*;
use crate::schema::CqlType;
pub(crate) fn extract_column_name(expr: &SelectExpression) -> Option<String> {
match expr {
SelectExpression::Column(col_ref) => Some(col_ref.column.clone()),
SelectExpression::Aliased(inner, _) if inner.is_aggregate() => None,
SelectExpression::Aliased(_, alias) => Some(alias.clone()),
_ => None,
}
}
pub(crate) fn projection_source_and_output(expr: &SelectExpression) -> Option<(String, String)> {
match expr {
SelectExpression::Column(col_ref) => Some((col_ref.column.clone(), col_ref.column.clone())),
SelectExpression::Aliased(inner, alias) => match inner.as_ref() {
SelectExpression::Column(col_ref) => Some((col_ref.column.clone(), alias.clone())),
_ => None,
},
_ => None,
}
}
pub(crate) fn unwrap_aggregate(expr: &SelectExpression) -> Option<&AggregateFunction> {
match expr {
SelectExpression::Aggregate(agg) => Some(agg),
SelectExpression::Aliased(inner, _) => unwrap_aggregate(inner),
_ => None,
}
}
pub(crate) fn aggregate_arg_source_columns(expr: &SelectExpression) -> Vec<String> {
let Some(agg) = unwrap_aggregate(expr) else {
return Vec::new();
};
agg.args
.iter()
.filter_map(|arg| match arg {
SelectExpression::Column(col_ref) if col_ref.column != "*" => {
Some(col_ref.column.clone())
}
_ => None,
})
.collect()
}
pub(crate) fn aggregate_output_name(expr: &SelectExpression) -> Option<String> {
match expr {
SelectExpression::Aggregate(agg) => Some(aggregate_column_and_alias(agg).1),
SelectExpression::Aliased(inner, alias) if inner.is_aggregate() => Some(alias.clone()),
_ => None,
}
}
pub(crate) fn result_column_name(expr: &SelectExpression, index: usize) -> String {
if let Some(name) = aggregate_output_name(expr) {
return name;
}
match expr {
SelectExpression::Column(col_ref) => col_ref.column.clone(),
SelectExpression::Aliased(_, alias) => alias.clone(),
_ => format!("col_{index}"),
}
}
pub(crate) fn aggregate_result_cql_type(
function: &AggregateType,
arg_type: Option<CqlType>,
) -> Option<CqlType> {
match function {
AggregateType::Count => Some(CqlType::BigInt),
AggregateType::Sum | AggregateType::Avg => Some(sum_avg_result_cql_type(arg_type)),
AggregateType::Min | AggregateType::Max => arg_type,
}
}
pub(crate) fn sum_avg_result_cql_type(arg_type: Option<CqlType>) -> CqlType {
match arg_type {
Some(t @ (CqlType::TinyInt | CqlType::SmallInt | CqlType::Int | CqlType::BigInt)) => t,
Some(CqlType::Counter) => CqlType::BigInt,
_ => CqlType::Double,
}
}
pub(crate) fn aggregate_column_and_alias(agg: &AggregateFunction) -> (String, String) {
let references_star = agg.args.is_empty()
|| agg
.args
.iter()
.any(|arg| matches!(arg, SelectExpression::Column(c) if c.column == "*"));
if references_star {
return ("*".to_string(), format!("{:?}(*)", agg.function));
}
match agg.args.first().and_then(extract_column_name) {
Some(col_name) => {
let alias = format!("{:?}_{}", agg.function, col_name);
(col_name, alias)
}
None => ("*".to_string(), format!("{:?}", agg.function)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn col(name: &str) -> SelectExpression {
SelectExpression::Column(ColumnRef {
table: None,
column: name.to_string(),
})
}
fn count_star() -> SelectExpression {
SelectExpression::Aggregate(AggregateFunction {
function: AggregateType::Count,
args: vec![],
distinct: false,
})
}
fn sum_of(c: &str) -> SelectExpression {
SelectExpression::Aggregate(AggregateFunction {
function: AggregateType::Sum,
args: vec![col(c)],
distinct: false,
})
}
#[test]
fn unaliased_aggregate_name_is_expression_text() {
assert_eq!(result_column_name(&count_star(), 0), "Count(*)");
assert_eq!(
aggregate_output_name(&count_star()).as_deref(),
Some("Count(*)")
);
assert_eq!(
result_column_name(&count_star(), 0),
aggregate_column_and_alias(unwrap_aggregate(&count_star()).unwrap()).1
);
assert_eq!(result_column_name(&sum_of("value"), 3), "Sum_value");
}
#[test]
fn aliased_aggregate_name_is_the_alias() {
let aliased = SelectExpression::Aliased(Box::new(count_star()), "total".to_string());
assert_eq!(result_column_name(&aliased, 0), "total");
assert_eq!(aggregate_output_name(&aliased).as_deref(), Some("total"));
assert!(unwrap_aggregate(&aliased).is_some());
}
#[test]
fn aliased_aggregate_is_not_a_projection_column() {
let aliased = SelectExpression::Aliased(Box::new(count_star()), "total".to_string());
assert_eq!(extract_column_name(&aliased), None);
assert_eq!(extract_column_name(&count_star()), None);
assert_eq!(extract_column_name(&col("name")).as_deref(), Some("name"));
let aliased_col = SelectExpression::Aliased(Box::new(col("name")), "n".to_string());
assert_eq!(extract_column_name(&aliased_col).as_deref(), Some("n"));
}
#[test]
fn aggregate_arg_source_columns_extracts_named_argument_only() {
assert_eq!(
aggregate_arg_source_columns(&sum_of("value")),
vec!["value"]
);
assert!(aggregate_arg_source_columns(&count_star()).is_empty());
let aliased = SelectExpression::Aliased(Box::new(sum_of("value")), "s".to_string());
assert_eq!(aggregate_arg_source_columns(&aliased), vec!["value"]);
assert!(aggregate_arg_source_columns(&col("value")).is_empty());
}
#[test]
fn aggregate_result_cql_type_derives_from_function_and_argument() {
assert_eq!(
aggregate_result_cql_type(&AggregateType::Count, Some(CqlType::Int)),
Some(CqlType::BigInt),
"COUNT is bigint regardless of any argument type"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Sum, Some(CqlType::Int)),
Some(CqlType::Int),
"SUM(int) is int"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Sum, Some(CqlType::BigInt)),
Some(CqlType::BigInt),
"SUM(bigint) is bigint"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Sum, Some(CqlType::SmallInt)),
Some(CqlType::SmallInt),
"SUM(smallint) stays smallint (Cassandra does not promote to int)"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Sum, Some(CqlType::TinyInt)),
Some(CqlType::TinyInt),
"SUM(tinyint) stays tinyint (Cassandra does not promote to int)"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Avg, Some(CqlType::Int)),
Some(CqlType::Int),
"AVG(int) is int"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Avg, Some(CqlType::BigInt)),
Some(CqlType::BigInt),
"AVG(bigint) is bigint"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Avg, Some(CqlType::SmallInt)),
Some(CqlType::SmallInt),
"AVG(smallint) stays smallint"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Avg, Some(CqlType::TinyInt)),
Some(CqlType::TinyInt),
"AVG(tinyint) stays tinyint"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Sum, Some(CqlType::Counter)),
Some(CqlType::BigInt),
"SUM(counter) is bigint"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Sum, Some(CqlType::Double)),
Some(CqlType::Double),
"SUM(double) stays double"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Avg, Some(CqlType::Float)),
Some(CqlType::Double),
"AVG(float) is double (no regression)"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Sum, None),
Some(CqlType::Double),
"SUM with unknown argument falls back to double"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Min, Some(CqlType::Int)),
Some(CqlType::Int),
"MIN preserves the argument column's type"
);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Max, Some(CqlType::Double)),
Some(CqlType::Double)
);
assert_eq!(aggregate_result_cql_type(&AggregateType::Min, None), None);
assert_eq!(
aggregate_result_cql_type(&AggregateType::Count, None),
Some(CqlType::BigInt)
);
}
#[test]
fn non_aggregate_naming_and_fallback() {
assert_eq!(aggregate_output_name(&col("x")), None);
assert_eq!(result_column_name(&col("x"), 2), "x");
assert_eq!(
result_column_name(
&SelectExpression::Literal(crate::types::Value::Integer(1)),
5
),
"col_5"
);
}
}