Skip to content

Commit

Permalink
Implement GREATEST function (gluesql#1312)
Browse files Browse the repository at this point in the history
The GREATEST function in SQL is used to select the largest (maximum) value from a list of expressions.
It compares each value and returns the highest value.

e.g.
GREATEST (expr1, expr2, expr3, ..., expr_n)
SELECT GREATEST (5, 10, 15);
  • Loading branch information
TheMan1697 committed Aug 5, 2023
1 parent f39316a commit 9666367
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 9 deletions.
19 changes: 19 additions & 0 deletions core/src/ast/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub enum Function {
selector: Expr,
},
GenerateUuid(),
Greatest(Vec<Expr>),
Format {
expr: Expr,
format: Expr,
Expand Down Expand Up @@ -351,6 +352,14 @@ impl ToSql for Function {
format!("UNWRAP({}, {})", expr.to_sql(), selector.to_sql())
}
Function::GenerateUuid() => "GENERATE_UUID()".to_owned(),
Function::Greatest(items) => {
let items = items
.iter()
.map(ToSql::to_sql)
.collect::<Vec<_>>()
.join(", ");
format!("GREATEST({})", items)
}
Function::Format { expr, format } => {
format!("FORMAT({}, {})", expr.to_sql(), format.to_sql())
}
Expand Down Expand Up @@ -1003,6 +1012,16 @@ mod tests {
&Expr::Function(Box::new(Function::GenerateUuid())).to_sql()
);

assert_eq!(
"GREATEST(16, 9, 7)",
&Expr::Function(Box::new(Function::Greatest(vec![
Expr::Literal(AstLiteral::Number(BigDecimal::from_str("16").unwrap())),
Expr::Literal(AstLiteral::Number(BigDecimal::from_str("9").unwrap())),
Expr::Literal(AstLiteral::Number(BigDecimal::from_str("7").unwrap()))
])))
.to_sql()
);

assert_eq!(
"FORMAT(DATE '2022-10-12', '%Y-%m')",
&Expr::Function(Box::new(Function::Format {
Expand Down
27 changes: 22 additions & 5 deletions core/src/ast_builder/expr/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ pub enum FunctionNode<'a> {
},
GetX(ExprNode<'a>),
GetY(ExprNode<'a>),
Greatest(ExprList<'a>),
CalcDistance {
geometry1: ExprNode<'a>,
geometry2: ExprNode<'a>,
Expand Down Expand Up @@ -366,6 +367,7 @@ impl<'a> TryFrom<FunctionNode<'a>> for Function {
}
FunctionNode::GetX(expr) => expr.try_into().map(Function::GetX),
FunctionNode::GetY(expr) => expr.try_into().map(Function::GetY),
FunctionNode::Greatest(expr_list) => expr_list.try_into().map(Function::Greatest),
FunctionNode::CalcDistance {
geometry1,
geometry2,
Expand Down Expand Up @@ -906,6 +908,10 @@ pub fn get_y<'a, T: Into<ExprNode<'a>>>(expr: T) -> ExprNode<'a> {
ExprNode::Function(Box::new(FunctionNode::GetY(expr.into())))
}

pub fn greatest<'a, T: Into<ExprList<'a>>>(exprs: T) -> ExprNode<'a> {
ExprNode::Function(Box::new(FunctionNode::Greatest(exprs.into())))
}

pub fn calc_distance<'a, T: Into<ExprNode<'a>>, U: Into<ExprNode<'a>>>(
geometry1: T,
geometry2: U,
Expand Down Expand Up @@ -935,11 +941,11 @@ mod tests {
ast_builder::{
abs, acos, ascii, asin, atan, calc_distance, cast, ceil, chr, coalesce, col, concat,
concat_ws, cos, date, degrees, divide, exp, expr, extract, find_idx, floor, format,
gcd, generate_uuid, get_x, get_y, ifnull, initcap, is_empty, last_day, lcm, left,
length, ln, log, log10, log2, lower, lpad, ltrim, md5, modulo, now, null, num, pi,
point, position, power, radians, rand, repeat, replace, reverse, right, round, rpad,
rtrim, sign, sin, skip, sqrt, substr, take, tan, test_expr, text, time, timestamp,
to_date, to_time, to_timestamp, upper,
gcd, generate_uuid, get_x, get_y, greatest, ifnull, initcap, is_empty, last_day, lcm,
left, length, ln, log, log10, log2, lower, lpad, ltrim, md5, modulo, now, null, num,
pi, point, position, power, radians, rand, repeat, replace, reverse, right, round,
rpad, rtrim, sign, sin, skip, sqrt, substr, take, tan, test_expr, text, time,
timestamp, to_date, to_time, to_timestamp, upper,
},
prelude::DataType,
};
Expand Down Expand Up @@ -1650,6 +1656,17 @@ mod tests {
test_expr(actual, expected);
}

#[test]
fn function_greatest() {
let actual = greatest(vec![num(1), num(2), num(3)]);
let expected = "GREATEST(1, 2, 3)";
test_expr(actual, expected);

let actual = greatest(vec![text("Glue"), text("SQL"), text("Go")]);
let expected = "GREATEST('Glue','SQL','Go')";
test_expr(actual, expected);
}

#[test]
fn function_calc_distance() {
let actual = calc_distance(point(num(1), num(2)), point(num(3), num(4)));
Expand Down
8 changes: 4 additions & 4 deletions core/src/ast_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ pub use expr::{
function::{
abs, acos, ascii, asin, atan, calc_distance, cast, ceil, chr, coalesce, concat, concat_ws,
cos, degrees, divide, exp, extract, find_idx, floor, format, gcd, generate_uuid, get_x,
get_y, ifnull, initcap, is_empty, last_day, lcm, left, length, ln, log, log10, log2, lower,
lpad, ltrim, md5, modulo, now, pi, point, position, power, radians, rand, repeat, replace,
reverse, right, round, rpad, rtrim, sign, sin, skip, sqrt, substr, take, tan, to_date,
to_time, to_timestamp, upper, FunctionNode,
get_y, greatest, ifnull, initcap, is_empty, last_day, lcm, left, length, ln, log, log10,
log2, lower, lpad, ltrim, md5, modulo, now, pi, point, position, power, radians, rand,
repeat, replace, reverse, right, round, rpad, rtrim, sign, sin, skip, sqrt, substr, take,
tan, to_date, to_time, to_timestamp, upper, FunctionNode,
},
};

Expand Down
6 changes: 6 additions & 0 deletions core/src/executor/evaluate/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ pub enum EvaluateError {

#[error("unsupported function: {0}")]
UnsupportedFunction(String),

#[error("The provided arguments are non-comparable: {0}")]
NonComparableArgumentError(String),

#[error("function requires at least one argument: {0}")]
FunctionRequiresAtLeastOneArgument(String),
}

fn error_serialize<S>(error: &chrono::format::ParseError, serializer: S) -> Result<S::Ok, S::Error>
Expand Down
18 changes: 18 additions & 0 deletions core/src/executor/evaluate/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,24 @@ pub fn generate_uuid<'a>() -> Evaluated<'a> {
Evaluated::from(Value::Uuid(Uuid::new_v4().as_u128()))
}

pub fn greatest(name: String, exprs: Vec<Evaluated<'_>>) -> Result<Evaluated<'_>> {
exprs
.into_iter()
.try_fold(None, |greatest, expr| -> Result<_> {
let greatest = match greatest {
Some(greatest) => greatest,
None => return Ok(Some(expr)),
};

match greatest.evaluate_cmp(&expr) {
Some(std::cmp::Ordering::Less) => Ok(Some(expr)),
Some(_) => Ok(Some(greatest)),
None => Err(EvaluateError::NonComparableArgumentError(name.to_owned()).into()),
}
})?
.ok_or(EvaluateError::FunctionRequiresAtLeastOneArgument(name.to_owned()).into())
}

pub fn format<'a>(
name: String,
expr: Evaluated<'_>,
Expand Down
4 changes: 4 additions & 0 deletions core/src/executor/evaluate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,10 @@ async fn evaluate_function<'a, 'b: 'a, 'c: 'a, T: GStore>(
f::unwrap(name, expr, selector)
}
Function::GenerateUuid() => Ok(f::generate_uuid()),
Function::Greatest(exprs) => {
let exprs = stream::iter(exprs).then(eval).try_collect().await?;
f::greatest(name, exprs)
}
Function::Now() => Ok(Evaluated::from(Value::Timestamp(Utc::now().naive_utc()))),
Function::Format { expr, format } => {
let expr = eval(expr).await?;
Expand Down
1 change: 1 addition & 0 deletions core/src/plan/expr/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ impl Function {
Self::ConcatWs { separator, exprs } => {
Exprs::VariableArgsWithSingle(once(separator).chain(exprs.iter()))
}
Self::Greatest(exprs) => Exprs::VariableArgs(exprs.iter()),
Self::Entries(expr) => Exprs::Single([expr].into_iter()),
}
}
Expand Down
8 changes: 8 additions & 0 deletions core/src/translate/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,14 @@ pub fn translate_function(sql_function: &SqlFunction) -> Result<Expr> {
let expr = translate_expr(args[0])?;
Ok(Expr::Function(Box::new(Function::IsEmpty(expr))))
}
"GREATEST" => {
check_len_min(name, args.len(), 2)?;
let exprs = args
.into_iter()
.map(translate_expr)
.collect::<Result<Vec<_>>>()?;
Ok(Expr::Function(Box::new(Function::Greatest(exprs))))
}
"VALUES" => {
check_len(name, args.len(), 1)?;

Expand Down
80 changes: 80 additions & 0 deletions test-suite/src/function/greatest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use {
crate::*,
gluesql_core::{
error::{EvaluateError, TranslateError},
prelude::Value::*,
},
};

test_case!(greatest, async move {
test!(
"SELECT GREATEST(1,6,9,7,0,10) AS goat;",
Ok(select!(
"goat"; I64; 10
))
);

test!(
"SELECT GREATEST(1.2,6.8,9.6,7.4,0.1,10.5) AS goat;",
Ok(select!(
"goat" ; F64; 10.5
))
);

test!(
"SELECT GREATEST('bibibik', 'babamba', 'melona') AS goat;",
Ok(select!(
"goat"; Str; "melona".to_owned()
))
);

macro_rules! date {
($date: expr) => {
$date.parse().unwrap()
};
}

test!(
"SELECT GREATEST(
DATE '2023-07-17',
DATE '2022-07-17',
DATE '2023-06-17',
DATE '2024-07-17',
DATE '2024-07-18') AS goat;",
Ok(select!(
"goat"; Date; date!("2024-07-18")
))
);

test!(
"SELECT GREATEST() AS goat;",
Err(TranslateError::FunctionArgsLengthNotMatchingMin {
name: "GREATEST".to_owned(),
expected_minimum: 2,
found: 0,
}
.into())
);

test!(
"SELECT GREATEST(1, 2, 'bibibik') AS goat;",
Err(EvaluateError::NonComparableArgumentError("GREATEST".to_owned()).into())
);

test!(
"SELECT GREATEST(NULL, 'bibibik', 'babamba', 'melona') AS goat;",
Err(EvaluateError::NonComparableArgumentError("GREATEST".to_owned()).into())
);

test!(
"SELECT GREATEST(NULL, NULL, NULL) AS goat;",
Err(EvaluateError::NonComparableArgumentError("GREATEST".to_owned()).into())
);

test!(
"SELECT GREATEST(true, false) AS goat;",
Ok(select!(
"goat"; Bool; true
))
);
});
1 change: 1 addition & 0 deletions test-suite/src/function/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub mod format;
pub mod gcd_lcm;
pub mod generate_uuid;
pub mod geometry;
pub mod greatest;
pub mod ifnull;
pub mod initcap;
pub mod is_empty;
Expand Down
1 change: 1 addition & 0 deletions test-suite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ macro_rules! generate_store_tests {
function_generate_uuid,
function::generate_uuid::generate_uuid
);
glue!(function_greatest, function::greatest::greatest);
glue!(type_match, type_match::type_match);
glue!(dictionary, dictionary::dictionary);
glue!(function_append, function::append::append);
Expand Down

0 comments on commit 9666367

Please sign in to comment.