Skip to content

Commit 412e6c2

Browse files
[ES|QL] Implicit numeric casting for CASE/GREATEST/LEAST (#122601)
* implicit numeric casting for conditional functions
1 parent 74f2113 commit 412e6c2

File tree

7 files changed

+389
-97
lines changed

7 files changed

+389
-97
lines changed

docs/changelog/122601.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 122601
2+
summary: Implicit numeric casting for CASE/GREATEST/LEAST
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 121890

x-pack/plugin/esql/qa/testFixtures/src/main/resources/conditional.csv-spec

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,111 @@ languages:integer| emp_no:integer|eval:keyword
281281
null |10020 |languages is null
282282
null |10021 |languages is null
283283
;
284+
285+
caseWithMixedNumericValue
286+
required_capability: mixed_numeric_types_in_case_greatest_least
287+
FROM employees
288+
| WHERE emp_no >= 10005 AND emp_no <= 10010
289+
| EVAL g = case(gender == "F", 1.0, gender == "M", 2, 3.0)
290+
| KEEP emp_no, gender, g
291+
| SORT emp_no
292+
;
293+
294+
emp_no:integer | gender:keyword | g:double
295+
10005 | M | 2.0
296+
10006 | F | 1.0
297+
10007 | F | 1.0
298+
10008 | M | 2.0
299+
10009 | F | 1.0
300+
10010 | null | 3.0
301+
;
302+
303+
caseWithMixedNumericValueWithNull
304+
required_capability: mixed_numeric_types_in_case_greatest_least
305+
FROM employees
306+
| WHERE emp_no >= 10005 AND emp_no <= 10010
307+
| EVAL g = case(gender == "F", 1.0, gender == "M", 2, null)
308+
| KEEP emp_no, gender, g
309+
| SORT emp_no
310+
;
311+
312+
emp_no:integer | gender:keyword | g:double
313+
10005 | M | 2.0
314+
10006 | F | 1.0
315+
10007 | F | 1.0
316+
10008 | M | 2.0
317+
10009 | F | 1.0
318+
10010 | null | null
319+
;
320+
321+
caseWithMixedNumericField
322+
required_capability: mixed_numeric_types_in_case_greatest_least
323+
FROM employees
324+
| WHERE emp_no >= 10005 AND emp_no <= 10010
325+
| EVAL g = case(gender == "F", height, gender == "M", salary, languages)
326+
| KEEP emp_no, gender, g
327+
| SORT emp_no
328+
;
329+
330+
emp_no:integer | gender:keyword | g:double
331+
10005 | M | 63528.0
332+
10006 | F | 1.56
333+
10007 | F | 1.7
334+
10008 | M | 43906.0
335+
10009 | F | 1.85
336+
10010 | null | 4.0
337+
;
338+
339+
caseWithMixedNumericFieldWithNull
340+
required_capability: mixed_numeric_types_in_case_greatest_least
341+
FROM employees
342+
| WHERE emp_no >= 10005 AND emp_no <= 10010
343+
| EVAL g = case(gender == "F", height, gender == "M", salary, null)
344+
| KEEP emp_no, gender, g
345+
| SORT emp_no
346+
;
347+
348+
emp_no:integer | gender:keyword | g:double
349+
10005 | M | 63528.0
350+
10006 | F | 1.56
351+
10007 | F | 1.7
352+
10008 | M | 43906.0
353+
10009 | F | 1.85
354+
10010 | null | null
355+
;
356+
357+
caseWithMixedNumericFieldWithMV
358+
required_capability: mixed_numeric_types_in_case_greatest_least
359+
FROM employees
360+
| WHERE emp_no >= 10005 AND emp_no <= 10010
361+
| EVAL g = case(gender == "F", salary_change, gender == "M", salary, languages)
362+
| KEEP emp_no, gender, g
363+
| SORT emp_no
364+
;
365+
366+
emp_no:integer | gender:keyword | g:double
367+
10005 | M | 63528.0
368+
10006 | F | -3.9
369+
10007 | F | [-7.06, 0.57, 1.99]
370+
10008 | M | 43906.0
371+
10009 | F | null
372+
10010 | null | 4.0
373+
;
374+
375+
caseWithMixedNumericFieldWithNullWithMV
376+
required_capability: mixed_numeric_types_in_case_greatest_least
377+
FROM employees
378+
| WHERE emp_no >= 10005 AND emp_no <= 10010
379+
| EVAL g = case(gender == "F", salary_change, gender == "M", salary, null)
380+
| KEEP emp_no, gender, g
381+
| SORT emp_no
382+
;
383+
384+
emp_no:integer | gender:keyword | g:double
385+
10005 | M | 63528.0
386+
10006 | F | -3.9
387+
10007 | F | [-7.06, 0.57, 1.99]
388+
10008 | M | 43906.0
389+
10009 | F | null
390+
10010 | null | null
391+
;

x-pack/plugin/esql/qa/testFixtures/src/main/resources/math.csv-spec

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,3 +1583,95 @@ emp_no: integer | x:date | y:date
15831583
10001 | 2024-11-03 | 2024-11-06
15841584
10002 | 2024-11-03 | 2024-11-06
15851585
;
1586+
1587+
greatestWithMixedNumericValues
1588+
required_capability: mixed_numeric_types_in_case_greatest_least
1589+
ROW g1=GREATEST(10.0, 5.0, 1, -100.1, 0, 1234, -10000), g2=GREATEST(10.0, 5, 1, -100.1, null);
1590+
1591+
g1:double |g2:double
1592+
1234 |null
1593+
;
1594+
1595+
leastWithMixedNumericValues
1596+
required_capability: mixed_numeric_types_in_case_greatest_least
1597+
ROW l1=LEAST(10.0, 5.0, 1, -100.1, 0, 1234, -10000), l2=LEAST(10.0, 5, 1, -100.1, null);
1598+
1599+
l1:double |l2:double
1600+
-10000 |null
1601+
;
1602+
1603+
greatestWithMixedNumericFields
1604+
required_capability: mixed_numeric_types_in_case_greatest_least
1605+
FROM employees
1606+
| EVAL g1 = GREATEST(height, salary, languages), g2 = GREATEST(height, salary, languages, null)
1607+
| KEEP emp_no, g1, g2
1608+
| SORT emp_no
1609+
| LIMIT 3
1610+
;
1611+
1612+
emp_no:integer | g1:double | g2:double
1613+
10001 | 57305.0 | null
1614+
10002 | 56371.0 | null
1615+
10003 | 61805.0 | null
1616+
;
1617+
1618+
leastWithMixedNumericFields
1619+
required_capability: mixed_numeric_types_in_case_greatest_least
1620+
FROM employees
1621+
| EVAL l1 = LEAST(height, salary, languages), l2 = LEAST(height, salary, languages, null)
1622+
| KEEP emp_no, l1, l2
1623+
| SORT emp_no
1624+
| LIMIT 3
1625+
;
1626+
1627+
emp_no:integer | l1:double | l2:double
1628+
10001 | 2.0 | null
1629+
10002 | 2.08 | null
1630+
10003 | 1.83 | null
1631+
;
1632+
1633+
greatestWithMixedNumericValuesWithMV
1634+
required_capability: mixed_numeric_types_in_case_greatest_least
1635+
ROW g1=GREATEST([10.0, 4], 1), g2=GREATEST([10.0, 4], 1, null);
1636+
1637+
g1:double |g2:double
1638+
10 |null
1639+
;
1640+
1641+
leastWithMixedNumericValuesWithMV
1642+
required_capability: mixed_numeric_types_in_case_greatest_least
1643+
ROW l1=LEAST([10.0, 4], 1), l2=LEAST([10.0, 4], 1, null);
1644+
1645+
l1:double |l2:double
1646+
1 |null
1647+
;
1648+
1649+
greatestWithMixedNumericFieldsWithMV
1650+
required_capability: mixed_numeric_types_in_case_greatest_least
1651+
FROM employees
1652+
| EVAL g1 = GREATEST(salary_change, salary, languages), g2 = GREATEST(salary_change, salary, languages, null)
1653+
| KEEP emp_no, g1, g2
1654+
| SORT emp_no
1655+
| LIMIT 3
1656+
;
1657+
1658+
emp_no:integer | g1:double | g2:double
1659+
10001 | 57305.0 | null
1660+
10002 | 56371.0 | null
1661+
10003 | 61805.0 | null
1662+
;
1663+
1664+
leastWithMixedNumericFieldsWithMV
1665+
required_capability: mixed_numeric_types_in_case_greatest_least
1666+
FROM employees
1667+
| EVAL l1 = LEAST(salary_change, salary, languages), l2 = LEAST(salary_change, salary, languages, null)
1668+
| KEEP emp_no, l1, l2
1669+
| SORT emp_no
1670+
| LIMIT 3
1671+
;
1672+
1673+
emp_no:integer | l1:double | l2:double
1674+
10001 | 1.19 | null
1675+
10002 | -7.23 | null
1676+
10003 | 4.0 | null
1677+
;

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,12 @@ public enum Cap {
840840
/**
841841
* Support for FORK command
842842
*/
843-
FORK(Build.current().isSnapshot());
843+
FORK(Build.current().isSnapshot()),
844+
845+
/**
846+
* Allow mixed numeric types in conditional functions - case, greatest and least
847+
*/
848+
MIXED_NUMERIC_TYPES_IN_CASE_GREATEST_LEAST;
844849

845850
private final boolean enabled;
846851

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
import org.elasticsearch.xpack.esql.expression.function.UnsupportedAttribute;
5353
import org.elasticsearch.xpack.esql.expression.function.grouping.GroupingFunction;
5454
import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction;
55+
import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case;
56+
import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Greatest;
57+
import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Least;
5558
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.AbstractConvertFunction;
5659
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.FoldablesConvertFunction;
5760
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToDouble;
@@ -1314,7 +1317,7 @@ private static Expression processIn(In in) {
13141317
}
13151318

13161319
private static boolean canCastMixedNumericTypes(org.elasticsearch.xpack.esql.core.expression.function.Function f) {
1317-
return f instanceof Coalesce;
1320+
return f instanceof Coalesce || f instanceof Case || f instanceof Greatest || f instanceof Least;
13181321
}
13191322

13201323
private static boolean canCastNumeric(DataType from, DataType to) {

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2330,13 +2330,37 @@ public void testRateRequiresCounterTypes() {
23302330
);
23312331
}
23322332

2333-
public void testCoalesceWithMixedNumericTypes() {
2333+
public void testConditionalFunctionsWithMixedNumericTypes() {
23342334
LogicalPlan plan = analyze("""
23352335
from test
23362336
| eval x = coalesce(salary_change, null, 0), y = coalesce(languages, null, 0), z = coalesce(languages.long, null, 0)
23372337
, w = coalesce(salary_change, null, 0::long)
23382338
| keep x, y, z, w
23392339
""", "mapping-default.json");
2340+
validateConditionalFunctions(plan);
2341+
2342+
plan = analyze("""
2343+
from test
2344+
| eval x = case(languages == 1, salary_change, languages == 2, salary, languages == 3, salary_change.long, 0)
2345+
, y = case(languages == 1, salary_change.int, languages == 2, salary, 0)
2346+
, z = case(languages == 1, salary_change.long, languages == 2, salary, 0::long)
2347+
, w = case(languages == 1, salary_change, languages == 2, salary, languages == 3, salary_change.long, null)
2348+
| keep x, y, z, w
2349+
""", "mapping-default.json");
2350+
validateConditionalFunctions(plan);
2351+
2352+
plan = analyze("""
2353+
from test
2354+
| eval x = greatest(salary_change, salary, salary_change.long)
2355+
, y = least(salary_change.int, salary)
2356+
, z = greatest(salary_change.long, salary, null)
2357+
, w = least(null, salary_change, salary_change.long, salary, null)
2358+
| keep x, y, z, w
2359+
""", "mapping-default.json");
2360+
validateConditionalFunctions(plan);
2361+
}
2362+
2363+
private void validateConditionalFunctions(LogicalPlan plan) {
23402364
var limit = as(plan, Limit.class);
23412365
var esqlProject = as(limit.child(), EsqlProject.class);
23422366
List<?> projections = esqlProject.projections();

0 commit comments

Comments
 (0)