Skip to content

Commit

Permalink
Implement COALESCE function (gluesql#1333)
Browse files Browse the repository at this point in the history
Returns the first non-NULL value.
Returns NULL if all arguments in the COALESCE function are NULL.

e.g.
SELECT COALESCE(NULL, 3, "test");
  • Loading branch information
cake-monotone committed Aug 5, 2023
1 parent 53f35cf commit 53b943f
Show file tree
Hide file tree
Showing 13 changed files with 348 additions and 11 deletions.
19 changes: 19 additions & 0 deletions core/src/ast/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub enum Function {
data_type: DataType,
},
Ceil(Expr),
Coalesce(Vec<Expr>),
Concat(Vec<Expr>),
ConcatWs {
separator: Expr,
Expand Down Expand Up @@ -229,6 +230,14 @@ impl ToSql for Function {
format!("CAST({} AS {data_type})", expr.to_sql())
}
Function::Ceil(e) => format!("CEIL({})", e.to_sql()),
Function::Coalesce(items) => {
let items = items
.iter()
.map(ToSql::to_sql)
.collect::<Vec<_>>()
.join(", ");
format!("COALESCE({items})")
}
Function::Concat(items) => {
let items = items
.iter()
Expand Down Expand Up @@ -632,6 +641,16 @@ mod tests {
.to_sql()
);

assert_eq!(
r#"COALESCE("First", NULL, "Last")"#,
&Expr::Function(Box::new(Function::Coalesce(vec![
Expr::Identifier("First".to_owned()),
Expr::Literal(AstLiteral::Null),
Expr::Identifier("Last".to_owned()),
])))
.to_sql()
);

assert_eq!(
"CONCAT(\"Tic\", \"tac\", \"toe\")",
&Expr::Function(Box::new(Function::Concat(vec![
Expand Down
26 changes: 19 additions & 7 deletions core/src/ast_builder/expr/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pub enum FunctionNode<'a> {
},
Degrees(ExprNode<'a>),
Radians(ExprNode<'a>),
Coalesce(ExprList<'a>),
Concat(ExprList<'a>),
ConcatWs {
separator: ExprNode<'a>,
Expand Down Expand Up @@ -260,6 +261,7 @@ impl<'a> TryFrom<FunctionNode<'a>> for Function {
let size = size.try_into()?;
Ok(Function::Rpad { expr, size, fill })
}
FunctionNode::Coalesce(expr_list) => expr_list.try_into().map(Function::Coalesce),
FunctionNode::Concat(expr_list) => expr_list.try_into().map(Function::Concat),
FunctionNode::ConcatWs { separator, exprs } => {
let separator = separator.try_into()?;
Expand Down Expand Up @@ -576,6 +578,9 @@ pub fn rand(expr: Option<ExprNode>) -> ExprNode {
pub fn round<'a, T: Into<ExprNode<'a>>>(expr: T) -> ExprNode<'a> {
ExprNode::Function(Box::new(FunctionNode::Round(expr.into())))
}
pub fn coalesce<'a, T: Into<ExprList<'a>>>(expr: T) -> ExprNode<'a> {
ExprNode::Function(Box::new(FunctionNode::Coalesce(expr.into())))
}
pub fn concat<'a, T: Into<ExprList<'a>>>(expr: T) -> ExprNode<'a> {
ExprNode::Function(Box::new(FunctionNode::Concat(expr.into())))
}
Expand Down Expand Up @@ -928,13 +933,13 @@ mod tests {
use crate::{
ast::DateTimeField,
ast_builder::{
abs, acos, ascii, asin, atan, calc_distance, cast, ceil, chr, 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, 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,
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,
},
prelude::DataType,
};
Expand Down Expand Up @@ -1271,6 +1276,13 @@ mod tests {
test_expr(actual, expected);
}

#[test]
fn function_coalesce() {
let actual = coalesce(vec![null(), text("Glue")]);
let expected = "COALESCE(NULL, 'Glue')";
test_expr(actual, expected);
}

#[test]
fn function_concat() {
let actual = concat(vec![text("Glue"), text("SQL"), text("Go")]);
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 @@ -73,10 +73,10 @@ pub use {index::CreateIndexNode, index::DropIndexNode};
pub use expr::{
aggregate::{avg, count, max, min, stdev, sum, variance, AggregateNode},
function::{
abs, acos, ascii, asin, atan, calc_distance, cast, ceil, chr, 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,
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,
},
Expand Down
7 changes: 7 additions & 0 deletions core/src/executor/evaluate/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ pub enum EvaluateError {
#[error("unsupported custom function in subqueries")]
UnsupportedCustomFunction,

#[error(r#"The function "{function_name}" requires at least {required_minimum} argument(s), but {found} were provided."#)]
FunctionRequiresMoreArguments {
function_name: String,
required_minimum: usize,
found: usize,
},

#[error("function args.length not matching: {name}, expected: {expected_minimum} ~ {expected_maximum}, found: {found}")]
FunctionArgsLengthNotWithinRange {
name: String,
Expand Down
25 changes: 25 additions & 0 deletions core/src/executor/evaluate/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,31 @@ pub fn calc_distance<'a>(x: Evaluated<'_>, y: Evaluated<'_>) -> Result<Evaluated
Ok(Evaluated::from(Value::F64(Point::calc_distance(&x, &y))))
}

pub fn coalesce<'a>(exprs: Vec<Evaluated<'_>>) -> Result<Evaluated<'a>> {
if exprs.is_empty() {
return Err((EvaluateError::FunctionRequiresMoreArguments {
function_name: "COALESCE".to_owned(),
required_minimum: 1,
found: exprs.len(),
})
.into());
}

let control_flow = exprs.into_iter().map(|expr| expr.try_into()).try_for_each(
|item: Result<Value>| match item {
Ok(value) if value.is_null() => ControlFlow::Continue(()),
Ok(value) => ControlFlow::Break(Ok(value)),
Err(err) => ControlFlow::Break(Err(err)),
},
);

match control_flow {
ControlFlow::Break(Ok(value)) => Ok(Evaluated::from(value)),
ControlFlow::Break(Err(err)) => Err(err),
ControlFlow::Continue(()) => Ok(Evaluated::from(Value::Null)),
}
}

pub fn length<'a>(name: String, expr: Evaluated<'_>) -> Result<Evaluated<'a>> {
match expr.try_into()? {
Value::Str(expr) => Ok(Evaluated::from(Value::U64(expr.chars().count() as u64))),
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 @@ -612,6 +612,10 @@ async fn evaluate_function<'a, 'b: 'a, 'c: 'a, T: GStore>(
let expr = eval(expr).await?;
f::extract(field, expr)
}
Function::Coalesce(exprs) => {
let exprs = stream::iter(exprs).then(eval).try_collect().await?;
f::coalesce(exprs)
}

// --- list ---
Function::Append { expr, value } => {
Expand Down
5 changes: 5 additions & 0 deletions core/src/plan/expr/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ impl Function {
start: Some(expr3),
} => Exprs::Triple([expr, expr2, expr3].into_iter()),
Self::Custom { name: _, exprs } => Exprs::VariableArgs(exprs.iter()),
Self::Coalesce(exprs) => Exprs::VariableArgs(exprs.iter()),
Self::Concat(exprs) => Exprs::VariableArgs(exprs.iter()),
Self::ConcatWs { separator, exprs } => {
Exprs::VariableArgsWithSingle(once(separator).chain(exprs.iter()))
Expand Down Expand Up @@ -308,6 +309,10 @@ mod tests {
);

//VariableArgs
test(r#"COALESCE("test")"#, &[r#""test""#]);

test(r#"COALESCE(NULL, "test")"#, &["NULL", r#""test""#]);

test(r#"CONCAT("abc")"#, &[r#""abc""#]);

test(r#"CONCAT("abc", "123")"#, &[r#""abc""#, r#""123""#]);
Expand Down
7 changes: 7 additions & 0 deletions core/src/translate/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,13 @@ pub fn translate_function(sql_function: &SqlFunction) -> Result<Expr> {
"AVG" => translate_aggregate_one_arg(Aggregate::Avg, args, name),
"VARIANCE" => translate_aggregate_one_arg(Aggregate::Variance, args, name),
"STDEV" => translate_aggregate_one_arg(Aggregate::Stdev, args, name),
"COALESCE" => {
let exprs = args
.into_iter()
.map(translate_expr)
.collect::<Result<Vec<_>>>()?;
Ok(Expr::Function(Box::new(Function::Coalesce(exprs))))
}
"CONCAT" => {
let exprs = args
.into_iter()
Expand Down
80 changes: 80 additions & 0 deletions test-suite/src/ast_builder/function/other/coalesce.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use {
crate::*,
chrono::{NaiveDate, NaiveDateTime},
gluesql_core::{
ast_builder::{self, *},
executor::Payload,
prelude::Value::*,
},
};

test_case!(coalesce, async move {
let glue = get_glue!();

// create table - Foo
let actual = table("Foo")
.create_table()
.add_column("id INTEGER PRIMARY KEY")
.add_column("first TEXT")
.add_column("second INTEGER")
.add_column("third TIMESTAMP")
.execute(glue)
.await;
let expected = Ok(Payload::Create);
assert_eq!(actual, expected, "create table - Foo");

// insert into Foo
let actual = table("Foo")
.insert()
.columns("id, first, second, third")
.values(vec![
vec![num(100), text("visible"), null(), null()],
vec![num(200), null(), num(42), null()],
vec![num(300), null(), null(), timestamp("2023-06-01 12:00:00")],
vec![num(400), null(), null(), null()],
])
.execute(glue)
.await;
let expected = Ok(Payload::Insert(4));
assert_eq!(actual, expected, "insert into Foo");

let actual = table("Foo")
.select()
.project("id")
.project(ast_builder::coalesce(vec![
null(),
col("first"),
col("second"),
col("third"),
]))
.order_by("id")
.execute(glue)
.await;
let expected = Ok(select_with_null!(
id | r#"COALESCE(NULL, "first", "second", "third")"#;
I64(100) Str("visible".to_owned());
I64(200) I64(42);
I64(300) Timestamp("2023-06-01T12:00:00".parse::<NaiveDateTime>().unwrap());
I64(400) Null
));
assert_eq!(actual, expected, "coalesce with table columns");

let actual = values(vec![
vec![ast_builder::coalesce(vec![text("뀨")])],
vec![ast_builder::coalesce(vec![null(), num(1)])],
vec![ast_builder::coalesce(vec![
null(),
null(),
date("2000-01-01"),
])],
])
.execute(glue)
.await;
let expected = Ok(select_with_null!(
column1;
Str("뀨".to_owned());
I64(1);
Date("2000-01-01".parse::<NaiveDate>().unwrap())
));
assert_eq!(actual, expected, "coalesce without table");
});
1 change: 1 addition & 0 deletions test-suite/src/ast_builder/function/other/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod coalesce;
pub mod ifnull;

0 comments on commit 53b943f

Please sign in to comment.