Browse Source

[ESQL] Support datetime data type in Least and Greatest functions (#113961) (#114130)

While working on Date Nanos, I noticed that Least and Greatest didn't have support for datetime. This PR corrects that and adds tests for it.

It seems to me that resolveType() is doing the wrong thing for these functions, as it accepts types that then do not have evaluator mappings, but refactoring that seems out of scope right now.

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Mark Tozzi 1 year ago
parent
commit
0c33257fee

+ 5 - 0
docs/changelog/113961.yaml

@@ -0,0 +1,5 @@
+pr: 113961
+summary: "[ESQL] Support datetime data type in Least and Greatest functions"
+area: ES|QL
+type: bug
+issues: []

+ 18 - 0
docs/reference/esql/functions/kibana/definition/greatest.json

@@ -35,6 +35,24 @@
       "variadic" : true,
       "returnType" : "boolean"
     },
+    {
+      "params" : [
+        {
+          "name" : "first",
+          "type" : "date",
+          "optional" : false,
+          "description" : "First of the columns to evaluate."
+        },
+        {
+          "name" : "rest",
+          "type" : "date",
+          "optional" : true,
+          "description" : "The rest of the columns to evaluate."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "date"
+    },
     {
       "params" : [
         {

+ 18 - 0
docs/reference/esql/functions/kibana/definition/least.json

@@ -34,6 +34,24 @@
       "variadic" : true,
       "returnType" : "boolean"
     },
+    {
+      "params" : [
+        {
+          "name" : "first",
+          "type" : "date",
+          "optional" : false,
+          "description" : "First of the columns to evaluate."
+        },
+        {
+          "name" : "rest",
+          "type" : "date",
+          "optional" : true,
+          "description" : "The rest of the columns to evaluate."
+        }
+      ],
+      "variadic" : true,
+      "returnType" : "date"
+    },
     {
       "params" : [
         {

+ 1 - 0
docs/reference/esql/functions/types/greatest.asciidoc

@@ -7,6 +7,7 @@
 first | rest | result
 boolean | boolean | boolean
 boolean | | boolean
+date | date | date
 double | double | double
 integer | integer | integer
 integer | | integer

+ 1 - 0
docs/reference/esql/functions/types/least.asciidoc

@@ -7,6 +7,7 @@
 first | rest | result
 boolean | boolean | boolean
 boolean | | boolean
+date | date | date
 double | double | double
 integer | integer | integer
 integer | | integer

+ 16 - 0
x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec

@@ -1270,3 +1270,19 @@ emp_no:integer | birth_date:datetime
 10007          | 1957-05-23T00:00:00Z
 10008          | 1958-02-19T00:00:00Z
 ;
+
+Least for dates
+required_capability: least_greatest_for_dates
+ROW a = LEAST(TO_DATETIME("1957-05-23T00:00:00Z"), TO_DATETIME("1958-02-19T00:00:00Z"));
+
+a:datetime
+1957-05-23T00:00:00
+;
+
+GREATEST for dates
+required_capability: least_greatest_for_dates
+ROW a = GREATEST(TO_DATETIME("1957-05-23T00:00:00Z"), TO_DATETIME("1958-02-19T00:00:00Z"));
+
+a:datetime
+1958-02-19T00:00:00
+;

+ 6 - 6
x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec

@@ -33,9 +33,9 @@ double e()
 "double exp(number:double|integer|long|unsigned_long)"
 "double|integer|long|unsigned_long floor(number:double|integer|long|unsigned_long)"
 "keyword from_base64(string:keyword|text)"
-"boolean|double|integer|ip|keyword|long|text|version greatest(first:boolean|double|integer|ip|keyword|long|text|version, ?rest...:boolean|double|integer|ip|keyword|long|text|version)"
+"boolean|date|double|integer|ip|keyword|long|text|version greatest(first:boolean|date|double|integer|ip|keyword|long|text|version, ?rest...:boolean|date|double|integer|ip|keyword|long|text|version)"
 "ip ip_prefix(ip:ip, prefixLengthV4:integer, prefixLengthV6:integer)"
-"boolean|double|integer|ip|keyword|long|text|version least(first:boolean|double|integer|ip|keyword|long|text|version, ?rest...:boolean|double|integer|ip|keyword|long|text|version)"
+"boolean|date|double|integer|ip|keyword|long|text|version least(first:boolean|date|double|integer|ip|keyword|long|text|version, ?rest...:boolean|date|double|integer|ip|keyword|long|text|version)"
 "keyword left(string:keyword|text, length:integer)"
 "integer length(string:keyword|text)"
 "integer locate(string:keyword|text, substring:keyword|text, ?start:integer)"
@@ -165,9 +165,9 @@ ends_with     |[str, suffix]                       |["keyword|text", "keyword|te
 exp           |number                              |"double|integer|long|unsigned_long"                                                                                               |Numeric expression. If `null`, the function returns `null`.
 floor         |number                              |"double|integer|long|unsigned_long"                                                                                               |Numeric expression. If `null`, the function returns `null`.
 from_base64   |string                              |"keyword|text"                                                                                                                    |A base64 string.
-greatest      |first                               |"boolean|double|integer|ip|keyword|long|text|version"                                                                             |First of the columns to evaluate.
+greatest      |first                               |"boolean|date|double|integer|ip|keyword|long|text|version"                                                                        |First of the columns to evaluate.
 ip_prefix     |[ip, prefixLengthV4, prefixLengthV6]|[ip, integer, integer]                                                                                                            |[IP address of type `ip` (both IPv4 and IPv6 are supported)., Prefix length for IPv4 addresses., Prefix length for IPv6 addresses.]
-least         |first                               |"boolean|double|integer|ip|keyword|long|text|version"                                                                             |First of the columns to evaluate.
+least         |first                               |"boolean|date|double|integer|ip|keyword|long|text|version"                                                                        |First of the columns to evaluate.
 left          |[string, length]                    |["keyword|text", integer]                                                                                                         |[The string from which to return a substring., The number of characters to return.]
 length        |string                              |"keyword|text"                                                                                                                    |String expression. If `null`, the function returns `null`.
 locate        |[string, substring, start]          |["keyword|text", "keyword|text", "integer"]                                                                                       |[An input string, A substring to locate in the input string, The start index]
@@ -431,9 +431,9 @@ ends_with     |boolean
 exp           |double                                                                                         |false                       |false           |false
 floor         |"double|integer|long|unsigned_long"                                                                                         |false                       |false           |false
 from_base64   |keyword                                                                                                                     |false                       |false           |false
-greatest      |"boolean|double|integer|ip|keyword|long|text|version"                                                                       |false                       |true            |false
+greatest      |"boolean|date|double|integer|ip|keyword|long|text|version"                                                                  |false                       |true            |false
 ip_prefix     |ip                                                                                                                          |[false, false, false]       |false           |false
-least         |"boolean|double|integer|ip|keyword|long|text|version"                                                                       |false                       |true            |false
+least         |"boolean|date|double|integer|ip|keyword|long|text|version"                                                                  |false                       |true            |false
 left          |keyword                                                                                                                     |[false, false]              |false           |false
 length        |integer                                                                                                                     |false                       |false           |false
 locate        |integer                                                                                                                     |[false, false, true]        |false           |false

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

@@ -278,6 +278,11 @@ public class EsqlCapabilities {
          */
         TO_DATE_NANOS(EsqlCorePlugin.DATE_NANOS_FEATURE_FLAG),
 
+        /**
+         * Support for datetime in least and greatest functions
+         */
+        LEAST_GREATEST_FOR_DATES,
+
         /**
          * Support CIDRMatch in CombineDisjunctions rule.
          */

+ 4 - 4
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Greatest.java

@@ -44,7 +44,7 @@ public class Greatest extends EsqlScalarFunction implements OptionalArgument {
     private DataType dataType;
 
     @FunctionInfo(
-        returnType = { "boolean", "double", "integer", "ip", "keyword", "long", "text", "version" },
+        returnType = { "boolean", "date", "double", "integer", "ip", "keyword", "long", "text", "version" },
         description = "Returns the maximum value from multiple columns. This is similar to <<esql-mv_max>>\n"
             + "except it is intended to run on multiple columns at once.",
         note = "When run on `keyword` or `text` fields, this returns the last string in alphabetical order. "
@@ -55,12 +55,12 @@ public class Greatest extends EsqlScalarFunction implements OptionalArgument {
         Source source,
         @Param(
             name = "first",
-            type = { "boolean", "double", "integer", "ip", "keyword", "long", "text", "version" },
+            type = { "boolean", "date", "double", "integer", "ip", "keyword", "long", "text", "version" },
             description = "First of the columns to evaluate."
         ) Expression first,
         @Param(
             name = "rest",
-            type = { "boolean", "double", "integer", "ip", "keyword", "long", "text", "version" },
+            type = { "boolean", "date", "double", "integer", "ip", "keyword", "long", "text", "version" },
             description = "The rest of the columns to evaluate.",
             optional = true
         ) List<Expression> rest
@@ -153,7 +153,7 @@ public class Greatest extends EsqlScalarFunction implements OptionalArgument {
         if (dataType == DataType.INTEGER) {
             return new GreatestIntEvaluator.Factory(source(), factories);
         }
-        if (dataType == DataType.LONG) {
+        if (dataType == DataType.LONG || dataType == DataType.DATETIME) {
             return new GreatestLongEvaluator.Factory(source(), factories);
         }
         if (dataType == DataType.KEYWORD

+ 4 - 4
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/Least.java

@@ -44,7 +44,7 @@ public class Least extends EsqlScalarFunction implements OptionalArgument {
     private DataType dataType;
 
     @FunctionInfo(
-        returnType = { "boolean", "double", "integer", "ip", "keyword", "long", "text", "version" },
+        returnType = { "boolean", "date", "double", "integer", "ip", "keyword", "long", "text", "version" },
         description = "Returns the minimum value from multiple columns. "
             + "This is similar to <<esql-mv_min>> except it is intended to run on multiple columns at once.",
         examples = @Example(file = "math", tag = "least")
@@ -53,12 +53,12 @@ public class Least extends EsqlScalarFunction implements OptionalArgument {
         Source source,
         @Param(
             name = "first",
-            type = { "boolean", "double", "integer", "ip", "keyword", "long", "text", "version" },
+            type = { "boolean", "date", "double", "integer", "ip", "keyword", "long", "text", "version" },
             description = "First of the columns to evaluate."
         ) Expression first,
         @Param(
             name = "rest",
-            type = { "boolean", "double", "integer", "ip", "keyword", "long", "text", "version" },
+            type = { "boolean", "date", "double", "integer", "ip", "keyword", "long", "text", "version" },
             description = "The rest of the columns to evaluate.",
             optional = true
         ) List<Expression> rest
@@ -152,7 +152,7 @@ public class Least extends EsqlScalarFunction implements OptionalArgument {
         if (dataType == DataType.INTEGER) {
             return new LeastIntEvaluator.Factory(source(), factories);
         }
-        if (dataType == DataType.LONG) {
+        if (dataType == DataType.LONG || dataType == DataType.DATETIME) {
             return new LeastLongEvaluator.Factory(source(), factories);
         }
         if (dataType == DataType.KEYWORD

+ 15 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/GreatestTests.java

@@ -100,6 +100,21 @@ public class GreatestTests extends AbstractScalarFunctionTestCase {
                 )
             )
         );
+        suppliers.add(
+            new TestCaseSupplier(
+                "(a, b)",
+                List.of(DataType.DATETIME, DataType.DATETIME),
+                () -> new TestCaseSupplier.TestCase(
+                    List.of(
+                        new TestCaseSupplier.TypedData(1727877348000L, DataType.DATETIME, "a"),
+                        new TestCaseSupplier.TypedData(1727790948000L, DataType.DATETIME, "b")
+                    ),
+                    "GreatestLongEvaluator[values=[MvMax[field=Attribute[channel=0]], MvMax[field=Attribute[channel=1]]]]",
+                    DataType.DATETIME,
+                    equalTo(1727877348000L)
+                )
+            )
+        );
         return parameterSuppliersFromTypedData(anyNullIsNull(false, suppliers));
     }
 

+ 15 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/conditional/LeastTests.java

@@ -99,6 +99,21 @@ public class LeastTests extends AbstractScalarFunctionTestCase {
                 )
             )
         );
+        suppliers.add(
+            new TestCaseSupplier(
+                "(a, b)",
+                List.of(DataType.DATETIME, DataType.DATETIME),
+                () -> new TestCaseSupplier.TestCase(
+                    List.of(
+                        new TestCaseSupplier.TypedData(1727877348000L, DataType.DATETIME, "a"),
+                        new TestCaseSupplier.TypedData(1727790948000L, DataType.DATETIME, "b")
+                    ),
+                    "LeastLongEvaluator[values=[MvMin[field=Attribute[channel=0]], MvMin[field=Attribute[channel=1]]]]",
+                    DataType.DATETIME,
+                    equalTo(1727790948000L)
+                )
+            )
+        );
         return parameterSuppliersFromTypedData(anyNullIsNull(false, suppliers));
     }