-
Notifications
You must be signed in to change notification settings - Fork 24.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ESQL: COALESCE function #98542
ESQL: COALESCE function #98542
Changes from 3 commits
6e9e5f8
429f01d
3311a5b
cacf69a
eb19d9a
f457f3d
c8ecc97
888e3d1
1307dab
d879684
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
[[esql-coalesce]] | ||
=== `COALESCE` | ||
|
||
Returns the first non-null value. | ||
|
||
[source.merge.styled,esql] | ||
---- | ||
include::{esql-specs}/nulls.csv-spec[tag=coalesce] | ||
---- | ||
[%header.monospaced.styled,format=dsv,separator=|] | ||
|=== | ||
include::{esql-specs}/null.csv-spec[tag=coalesce-result] | ||
|=== |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
isNull | ||
from employees | ||
| where is_null(gender) | ||
| sort first_name | ||
| keep first_name, gender | ||
| limit 3; | ||
|
||
first_name:keyword|gender:keyword | ||
Berni |null | ||
Cristinel |null | ||
Duangkaew |null | ||
; | ||
|
||
notIsNull | ||
from employees | ||
| where not is_null(gender) | ||
| sort first_name | ||
| keep first_name, gender | ||
| limit 3; | ||
|
||
first_name:keyword|gender:keyword | ||
Alejandro |F | ||
Amabile |M | ||
Anneke |F | ||
; | ||
|
||
coalesceSimple | ||
// tag::coalesce[] | ||
ROW a=null, b="b" | ||
| EVAL COALESCE(a, b) | ||
// end::coalesce[] | ||
; | ||
|
||
// tag::coalesce-result[] | ||
a:null | b:keyword | COALESCE(a,b):keyword | ||
null | b | b | ||
// end::coalesce-result[] | ||
; | ||
|
||
coalesce | ||
FROM employees | ||
| EVAL first_name = COALESCE(first_name, "X") | ||
| SORT first_name DESC, emp_no ASC | ||
| KEEP emp_no, first_name | ||
| limit 10; | ||
|
||
emp_no:integer | first_name:keyword | ||
10047 | Zvonko | ||
10081 | Zhongwei | ||
10026 | Yongqiao | ||
10043 | Yishay | ||
10050 | Yinghua | ||
10087 | Xinglin | ||
10030 | X | ||
10031 | X | ||
10032 | X | ||
10033 | X | ||
; | ||
|
||
coalesceBackwards | ||
FROM employees | ||
| EVAL first_name = COALESCE("X", first_name) | ||
| SORT first_name DESC, emp_no ASC | ||
| KEEP emp_no, first_name | ||
| limit 10; | ||
|
||
emp_no:integer | first_name:keyword | ||
10001 | X | ||
10002 | X | ||
10003 | X | ||
10004 | X | ||
10005 | X | ||
10006 | X | ||
10007 | X | ||
10008 | X | ||
10009 | X | ||
10010 | X | ||
; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,6 +65,7 @@ | |
import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMedian; | ||
import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvMin; | ||
import org.elasticsearch.xpack.esql.expression.function.scalar.multivalue.MvSum; | ||
import org.elasticsearch.xpack.esql.expression.function.scalar.nulls.Coalesce; | ||
import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat; | ||
import org.elasticsearch.xpack.esql.expression.function.scalar.string.Length; | ||
import org.elasticsearch.xpack.esql.expression.function.scalar.string.Split; | ||
|
@@ -140,7 +141,9 @@ private FunctionDefinition[][] functions() { | |
def(DateTrunc.class, DateTrunc::new, "date_trunc"), | ||
def(Now.class, Now::new, "now") }, | ||
// conditional | ||
new FunctionDefinition[] { def(Case.class, Case::new, "case"), def(IsNull.class, IsNull::new, "is_null"), }, | ||
new FunctionDefinition[] { def(Case.class, Case::new, "case") }, | ||
// null | ||
new FunctionDefinition[] { def(Coalesce.class, Coalesce::new, "coalesce"), def(IsNull.class, IsNull::new, "is_null"), }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've just removed the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ++. I'll get that merged. |
||
// IP | ||
new FunctionDefinition[] { def(CIDRMatch.class, CIDRMatch::new, "cidr_match") }, | ||
// conversion functions | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.esql.expression.function.scalar.nulls; | ||
|
||
import org.elasticsearch.compute.data.Block; | ||
import org.elasticsearch.compute.data.ElementType; | ||
import org.elasticsearch.compute.data.Page; | ||
import org.elasticsearch.compute.operator.EvalOperator; | ||
import org.elasticsearch.xpack.esql.planner.LocalExecutionPlanner; | ||
import org.elasticsearch.xpack.esql.planner.Mappable; | ||
import org.elasticsearch.xpack.ql.expression.Expression; | ||
import org.elasticsearch.xpack.ql.expression.Nullability; | ||
import org.elasticsearch.xpack.ql.expression.TypeResolutions; | ||
import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction; | ||
import org.elasticsearch.xpack.ql.expression.gen.script.ScriptTemplate; | ||
import org.elasticsearch.xpack.ql.tree.NodeInfo; | ||
import org.elasticsearch.xpack.ql.tree.Source; | ||
import org.elasticsearch.xpack.ql.type.DataType; | ||
|
||
import java.util.List; | ||
import java.util.function.Function; | ||
import java.util.function.Supplier; | ||
import java.util.stream.IntStream; | ||
|
||
import static org.elasticsearch.xpack.ql.type.DataTypes.NULL; | ||
|
||
/** | ||
* Function returning the first non-null value. | ||
*/ | ||
public class Coalesce extends ScalarFunction implements Mappable { | ||
private DataType dataType; | ||
|
||
public Coalesce(Source source, List<Expression> expressions) { | ||
super(source, expressions); | ||
} | ||
|
||
@Override | ||
public DataType dataType() { | ||
if (dataType == null) { | ||
resolveType(); | ||
} | ||
return dataType; | ||
} | ||
|
||
@Override | ||
protected TypeResolution resolveType() { | ||
if (childrenResolved() == false) { | ||
return new TypeResolution("Unresolved children"); | ||
} | ||
|
||
for (int position = 0; position < children().size(); position++) { | ||
if (dataType == null || dataType == NULL) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this check we have |
||
dataType = children().get(position).dataType(); | ||
continue; | ||
} | ||
TypeResolution resolution = TypeResolutions.isType( | ||
children().get(position), | ||
t -> t == dataType, | ||
sourceText(), | ||
TypeResolutions.ParamOrdinal.fromIndex(position), | ||
dataType.typeName() | ||
); | ||
if (resolution.unresolved()) { | ||
return resolution; | ||
} | ||
} | ||
return TypeResolution.TYPE_RESOLVED; | ||
} | ||
|
||
@Override | ||
public Nullability nullable() { | ||
return children().get(children().size() - 1).nullable(); | ||
} | ||
|
||
@Override | ||
public ScriptTemplate asScript() { | ||
throw new UnsupportedOperationException(); | ||
} | ||
|
||
@Override | ||
public Expression replaceChildren(List<Expression> newChildren) { | ||
return new Coalesce(source(), newChildren); | ||
} | ||
|
||
@Override | ||
protected NodeInfo<? extends Expression> info() { | ||
return NodeInfo.create(this, Coalesce::new, children()); | ||
} | ||
|
||
@Override | ||
public boolean foldable() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In ES SQL there is a Coalesce function which is interesting because it's also coming together with an Optimizer rule and the Otherwise, there is The way I see it:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll open #98612 and replace with |
||
for (Expression c : children()) { | ||
if (c.foldable() == false) { | ||
return false; | ||
} | ||
if (c.fold() != null) { // TODO is this ok? | ||
return true; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
@Override | ||
public Object fold() { | ||
return Mappable.super.fold(); | ||
} | ||
|
||
@Override | ||
public Supplier<EvalOperator.ExpressionEvaluator> toEvaluator( | ||
Function<Expression, Supplier<EvalOperator.ExpressionEvaluator>> toEvaluator | ||
) { | ||
List<Supplier<EvalOperator.ExpressionEvaluator>> evaluatorSuppliers = children().stream().map(toEvaluator).toList(); | ||
return () -> new CoalesceEvaluator( | ||
LocalExecutionPlanner.toElementType(dataType()), | ||
evaluatorSuppliers.stream().map(Supplier::get).toList() | ||
); | ||
} | ||
|
||
private record CoalesceEvaluator(ElementType resultType, List<EvalOperator.ExpressionEvaluator> evaluators) | ||
implements | ||
EvalOperator.ExpressionEvaluator { | ||
@Override | ||
public Block eval(Page page) { | ||
// Evaluate row at a time for now because its simpler. Much slower. But simpler. | ||
int positionCount = page.getPositionCount(); | ||
Block.Builder result = resultType.newBlockBuilder(positionCount); | ||
position: for (int p = 0; p < positionCount; p++) { | ||
int[] positions = new int[] { p }; | ||
Page limited = new Page( | ||
1, | ||
IntStream.range(0, page.getBlockCount()).mapToObj(b -> page.getBlock(b).filter(positions)).toArray(Block[]::new) | ||
); | ||
for (EvalOperator.ExpressionEvaluator eval : evaluators) { | ||
Block e = eval.eval(limited); | ||
if (false == e.isNull(0)) { | ||
result.copyFrom(e, 0, 1); | ||
continue position; | ||
} | ||
} | ||
result.appendNull(); | ||
} | ||
return result.build(); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though Coalesce can have an arbitrary number of parameters, the tests here always use two.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
y=COALESCE(true, false, null)
doesn't seem to work. The value fory
isnull
. Only if I the last value is non-null it works.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh. I thought I had tests for all that in the unit tests. I'll add more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting! the break here was with an optimizer rule. I'll push a fix.