Browse Source

ES|QL: Improve support for TEXT fields in functions (#106688)

Luigi Dell'Aquila 1 year ago
parent
commit
62e3e5fd1b
24 changed files with 386 additions and 36 deletions
  1. 5 0
      docs/changelog/106688.yaml
  2. 7 0
      docs/reference/esql/functions/description/date_format.asciidoc
  3. 14 0
      docs/reference/esql/functions/layout/date_format.asciidoc
  4. 7 0
      docs/reference/esql/functions/parameters/date_format.asciidoc
  5. 1 0
      docs/reference/esql/functions/signature/date_format.svg
  6. 1 0
      docs/reference/esql/functions/types/date_extract.asciidoc
  7. 10 0
      docs/reference/esql/functions/types/date_format.asciidoc
  8. 1 0
      docs/reference/esql/functions/types/date_parse.asciidoc
  9. 9 9
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/meta.csv-spec
  10. 8 0
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/string.csv-spec
  11. 9 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/EsqlTypeResolutions.java
  12. 4 3
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtract.java
  13. 7 12
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormat.java
  14. 4 3
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java
  15. 5 6
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ip/CIDRMatch.java
  16. 2 3
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Split.java
  17. 1 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java
  18. 3 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractScalarFunctionTestCase.java
  19. 12 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java
  20. 79 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java
  21. 12 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseTests.java
  22. 104 0
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/ip/CIDRMatchTests.java
  23. 61 0
      x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/100_bug_fix.yml
  24. 20 0
      x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/80_text.yml

+ 5 - 0
docs/changelog/106688.yaml

@@ -0,0 +1,5 @@
+pr: 106688
+summary: "ES|QL: Improve support for TEXT fields in functions"
+area: ES|QL
+type: bug
+issues: []

+ 7 - 0
docs/reference/esql/functions/description/date_format.asciidoc

@@ -0,0 +1,7 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Description*
+
+Returns a string representation of a date, in the provided format.
+
+NOTE: 

+ 14 - 0
docs/reference/esql/functions/layout/date_format.asciidoc

@@ -0,0 +1,14 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+[discrete]
+[[esql-date_format]]
+=== `DATE_FORMAT`
+
+*Syntax*
+
+[.text-center]
+image::esql/functions/signature/date_format.svg[Embedded,opts=inline]
+
+include::../parameters/date_format.asciidoc[]
+include::../description/date_format.asciidoc[]
+include::../types/date_format.asciidoc[]

+ 7 - 0
docs/reference/esql/functions/parameters/date_format.asciidoc

@@ -0,0 +1,7 @@
+*Parameters*
+
+`dateFormat`::
+A valid date pattern
+
+`date`::
+Date expression

+ 1 - 0
docs/reference/esql/functions/signature/date_format.svg

@@ -0,0 +1 @@
+<svg version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="516" height="46" viewbox="0 0 516 46"><defs><style type="text/css">#guide .c{fill:none;stroke:#222222;}#guide .k{fill:#000000;font-family:Roboto Mono,Sans-serif;font-size:20px;}#guide .s{fill:#e4f4ff;stroke:#222222;}#guide .syn{fill:#8D8D8D;font-family:Roboto Mono,Sans-serif;font-size:20px;}</style></defs><path class="c" d="M0 31h5m152 0h10m32 0h10m140 0h10m32 0h10m68 0h10m32 0h5"/><rect class="s" x="5" y="5" width="152" height="36"/><text class="k" x="15" y="31">DATE_FORMAT</text><rect class="s" x="167" y="5" width="32" height="36" rx="7"/><text class="syn" x="177" y="31">(</text><rect class="s" x="209" y="5" width="140" height="36" rx="7"/><text class="k" x="219" y="31">dateFormat</text><rect class="s" x="359" y="5" width="32" height="36" rx="7"/><text class="syn" x="369" y="31">,</text><rect class="s" x="401" y="5" width="68" height="36" rx="7"/><text class="k" x="411" y="31">date</text><rect class="s" x="479" y="5" width="32" height="36" rx="7"/><text class="syn" x="489" y="31">)</text></svg>

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

@@ -6,4 +6,5 @@
 |===
 datePart | date | result
 keyword | datetime | long
+text | datetime | long
 |===

+ 10 - 0
docs/reference/esql/functions/types/date_format.asciidoc

@@ -0,0 +1,10 @@
+// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.
+
+*Supported types*
+
+[%header.monospaced.styled,format=dsv,separator=|]
+|===
+dateFormat | date | result
+keyword | datetime | keyword
+text | datetime | keyword
+|===

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

@@ -7,4 +7,5 @@
 datePattern | dateString | result
 keyword | keyword | datetime
 keyword | text | datetime
+text | text | datetime
 |===

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

@@ -12,7 +12,7 @@ auto_bucket              |"double|date auto_bucket(field:integer|long|double|dat
 avg                      |"double avg(number:double|integer|long)"                                           |number                     |"double|integer|long"                 |   ""                                               |double                    | "The average of a numeric field."                      | false                | false | true
 case                     |"boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version case(condition:boolean, trueValue...:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)"                               |[condition, trueValue]             |["boolean", "boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version"]            |["", ""]                                            |"boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version"                    | "Accepts pairs of conditions and values. The function returns the value that belongs to the first condition that evaluates to true."                      | [false, false]       | true | false
 ceil                     |"double|integer|long|unsigned_long ceil(number:double|integer|long|unsigned_long)"     |number                        |"double|integer|long|unsigned_long"                 | "Numeric expression. If `null`, the function returns `null`." | "double|integer|long|unsigned_long"                    | "Round a number up to the nearest integer."                      | false                | false | false
-cidr_match               |boolean cidr_match(ip:ip, blockX...:keyword)                         |[ip, blockX]             |[ip, keyword]            |["", "CIDR block to test the IP against."]                                            |boolean                    | "Returns true if the provided IP is contained in one of the provided CIDR blocks."                      | [false, false]       | true | false
+cidr_match               |"boolean cidr_match(ip:ip, blockX...:keyword|text)"                         |[ip, blockX]             |[ip, "keyword|text"]            |["", "CIDR block to test the IP against."]                                            |boolean                    | "Returns true if the provided IP is contained in one of the provided CIDR blocks."                      | [false, false]       | true | false
 coalesce                 |"boolean|text|integer|keyword|long coalesce(first:boolean|text|integer|keyword|long, ?rest...:boolean|text|integer|keyword|long)"                           |first             | "boolean|text|integer|keyword|long"            | "Expression to evaluate"                                            |"boolean|text|integer|keyword|long"                    | "Returns the first of its arguments that is not null. If all arguments are null, it returns `null`."                      | false       | true | false
 concat                   |"keyword concat(string1:keyword|text, string2...:keyword|text)"                             |[string1, string2]             |["keyword|text", "keyword|text"]            |["", ""]                                            |keyword                    | "Concatenates two or more strings."                      | [false, false]       | true | false
 cos                      |"double cos(number:double|integer|long|unsigned_long)"      |number             |"double|integer|long|unsigned_long"                 | "An angle, in radians"  |double         | "Returns the trigonometric cosine of an angle"                      | false                | false | false
@@ -20,10 +20,10 @@ cosh                     |"double cosh(number:double|integer|long|unsigned_long)
 count                    |"long count(?field:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)"                                         |field                     |"boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version"                 | "Column or literal for which to count the number of values."                                                 |long                    | "Returns the total number (count) of input values."                      | true                | false | true
 count_distinct           |"long count_distinct(field:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|version, ?precision:integer)"                        |[field, precision]             |["boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|version, integer"]            |["Column or literal for which to count the number of distinct values.", ""]                                            |long                    | "Returns the approximate number of distinct values."                      | [false, true]       | false | true
 date_diff                |"integer date_diff(unit:keyword|text, startTimestamp:date, endTimestamp:date)"|[unit, startTimestamp, endTimestamp] |["keyword|text", "date", "date"]  |["A valid date unit", "A string representing a start timestamp", "A string representing an end timestamp"] |integer | "Subtract 2 dates and return their difference in multiples of a unit specified in the 1st argument" | [false, false, false] | false | false
-date_extract             |long date_extract(datePart:keyword, date:date)                          |[datePart, date]             |[keyword, date]            |["Part of the date to extract. Can be: aligned_day_of_week_in_month; aligned_day_of_week_in_year; aligned_week_of_month; aligned_week_of_year; ampm_of_day; clock_hour_of_ampm; clock_hour_of_day; day_of_month; day_of_week; day_of_year; epoch_day; era; hour_of_ampm; hour_of_day; instant_seconds; micro_of_day; micro_of_second; milli_of_day; milli_of_second; minute_of_day; minute_of_hour; month_of_year; nano_of_day; nano_of_second; offset_seconds; proleptic_month; second_of_day; second_of_minute; year; or year_of_era.", "Date expression"]                                            |long                    | "Extracts parts of a date, like year, month, day, hour."                      | [false, false]       | false | false
-date_format              |keyword date_format(?dateFormat:keyword, date:date)                           |[dateFormat, date]             |[keyword, date]            |["A valid date pattern", "Date expression"]                                            |keyword                    | "Returns a string representation of a date, in the provided format."                      | [true, false]       | false | false
-date_parse               |"date date_parse(?datePattern:keyword, dateString:keyword|text)"|[datePattern, dateString]|["keyword", "keyword|text"]|["A valid date pattern", "A string representing a date"]|date                 |Parses a string into a date value | [true, false]       | false          | false
-date_trunc               |"date date_trunc(interval:keyword, date:date)"                            |[interval, date]             |[keyword, date]            |["Interval; expressed using the timespan literal syntax.", "Date expression"]                                            |date                    | "Rounds down a date to the closest interval."                      | [false, false]       | false | false
+date_extract             |"long date_extract(datePart:keyword|text, date:date)"                          |[datePart, date]             |["keyword|text", date]            |["Part of the date to extract. Can be: aligned_day_of_week_in_month; aligned_day_of_week_in_year; aligned_week_of_month; aligned_week_of_year; ampm_of_day; clock_hour_of_ampm; clock_hour_of_day; day_of_month; day_of_week; day_of_year; epoch_day; era; hour_of_ampm; hour_of_day; instant_seconds; micro_of_day; micro_of_second; milli_of_day; milli_of_second; minute_of_day; minute_of_hour; month_of_year; nano_of_day; nano_of_second; offset_seconds; proleptic_month; second_of_day; second_of_minute; year; or year_of_era.", "Date expression"]                                            |long                    | "Extracts parts of a date, like year, month, day, hour."                      | [false, false]       | false | false
+date_format              |"keyword date_format(?dateFormat:keyword|text, date:date)"                           |[dateFormat, date]             |["keyword|text", date]            |["A valid date pattern", "Date expression"]                                            |keyword                    | "Returns a string representation of a date, in the provided format."                      | [true, false]       | false | false
+date_parse               |"date date_parse(?datePattern:keyword|text, dateString:keyword|text)"|[datePattern, dateString]|["keyword|text", "keyword|text"]|["A valid date pattern", "A string representing a date"]|date                 |Parses a string into a date value | [true, false]       | false          | false
+date_trunc               |"date date_trunc(interval:keyword, date:date)"                            |[interval, date]             |["keyword", date]            |["Interval; expressed using the timespan literal syntax.", "Date expression"]                                            |date                    | "Rounds down a date to the closest interval."                      | [false, false]       | false | false
 e                        |double e()                                                   | null                    | null             | null                                               |double                    | "Euler’s number."                      | null                 | false | false
 ends_with                |"boolean ends_with(str:keyword|text, suffix:keyword|text)"                             |[str, suffix]             |["keyword|text", "keyword|text"]            |["", ""]                                            |boolean                    | "Returns a boolean that indicates whether a keyword string ends with another string"                      | [false, false]       | false | false
 floor                    |"double|integer|long|unsigned_long floor(number:double|integer|long|unsigned_long)"    |number                       |"double|integer|long|unsigned_long"    | ""                                                 |"double|integer|long|unsigned_long"                    | "Round a number down to the nearest integer."                      | false                | false | false
@@ -116,7 +116,7 @@ synopsis:keyword
 "double avg(number:double|integer|long)"
 "boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version case(condition:boolean, trueValue...:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)"
 "double|integer|long|unsigned_long ceil(number:double|integer|long|unsigned_long)"
-boolean cidr_match(ip:ip, blockX...:keyword)
+"boolean cidr_match(ip:ip, blockX...:keyword|text)"
 "boolean|text|integer|keyword|long coalesce(first:boolean|text|integer|keyword|long, ?rest...:boolean|text|integer|keyword|long)"
 "keyword concat(string1:keyword|text, string2...:keyword|text)"
 "double cos(number:double|integer|long|unsigned_long)"
@@ -124,9 +124,9 @@ boolean cidr_match(ip:ip, blockX...:keyword)
 "long count(?field:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)"
 "long count_distinct(field:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|version, ?precision:integer)"
 "integer date_diff(unit:keyword|text, startTimestamp:date, endTimestamp:date)"
-long date_extract(datePart:keyword, date:date)
-keyword date_format(?dateFormat:keyword, date:date)
-"date date_parse(?datePattern:keyword, dateString:keyword|text)"
+"long date_extract(datePart:keyword|text, date:date)"
+"keyword date_format(?dateFormat:keyword|text, date:date)"
+"date date_parse(?datePattern:keyword|text, dateString:keyword|text)"
 "date date_trunc(interval:keyword, date:date)"
 double e()
 "boolean ends_with(str:keyword|text, suffix:keyword|text)"

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

@@ -1160,3 +1160,11 @@ required_feature: esql.agg_values
                                                                                              null | null
 // end::values-grouped-result[]
 ;
+
+
+splitBasedOnField
+from employees | where emp_no == 10001 | eval split = split("fooMbar", gender) | keep gender, split;
+
+gender:keyword | split:keyword 
+M              | [foo, bar]
+;

+ 9 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/EsqlTypeResolutions.java

@@ -27,6 +27,15 @@ import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isType;
 
 public class EsqlTypeResolutions {
 
+    public static Expression.TypeResolution isStringAndExact(Expression e, String operationName, TypeResolutions.ParamOrdinal paramOrd) {
+        Expression.TypeResolution resolution = TypeResolutions.isString(e, operationName, paramOrd);
+        if (resolution.unresolved()) {
+            return resolution;
+        }
+
+        return isExact(e, operationName, paramOrd);
+    }
+
     public static Expression.TypeResolution isExact(Expression e, String operationName, TypeResolutions.ParamOrdinal paramOrd) {
         if (e instanceof FieldAttribute fa) {
             if (DataTypes.isString(fa.dataType())) {

+ 4 - 3
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtract.java

@@ -14,6 +14,7 @@ import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator;
 import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
 import org.elasticsearch.xpack.esql.expression.function.Param;
 import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlConfigurationFunction;
+import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
 import org.elasticsearch.xpack.ql.InvalidArgumentException;
 import org.elasticsearch.xpack.ql.expression.Expression;
 import org.elasticsearch.xpack.ql.expression.TypeResolutions;
@@ -28,10 +29,10 @@ import java.time.temporal.ChronoField;
 import java.util.List;
 import java.util.function.Function;
 
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isStringAndExact;
 import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.EsqlConverter.STRING_TO_CHRONO_FIELD;
 import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.chronoToLong;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isDate;
-import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
 
 public class DateExtract extends EsqlConfigurationFunction {
 
@@ -42,7 +43,7 @@ public class DateExtract extends EsqlConfigurationFunction {
         Source source,
         // Need to replace the commas in the description here with semi-colon as there's a bug in the CSV parser
         // used in the CSVTests and fixing it is not trivial
-        @Param(name = "datePart", type = { "keyword" }, description = """
+        @Param(name = "datePart", type = { "keyword", "text" }, description = """
             Part of the date to extract.
             Can be: aligned_day_of_week_in_month; aligned_day_of_week_in_year; aligned_week_of_month;
             aligned_week_of_year; ampm_of_day; clock_hour_of_ampm; clock_hour_of_day; day_of_month; day_of_week;
@@ -76,7 +77,7 @@ public class DateExtract extends EsqlConfigurationFunction {
         if (chronoField == null) {
             Expression field = children().get(0);
             try {
-                if (field.foldable() && field.dataType() == DataTypes.KEYWORD) {
+                if (field.foldable() && EsqlDataTypes.isString(field.dataType())) {
                     chronoField = (ChronoField) STRING_TO_CHRONO_FIELD.convert(field.fold());
                 }
             } catch (Exception e) {

+ 7 - 12
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormat.java

@@ -16,6 +16,7 @@ import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
 import org.elasticsearch.xpack.esql.expression.function.Param;
 import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlConfigurationFunction;
 import org.elasticsearch.xpack.esql.session.EsqlConfiguration;
+import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
 import org.elasticsearch.xpack.ql.expression.Expression;
 import org.elasticsearch.xpack.ql.expression.function.OptionalArgument;
 import org.elasticsearch.xpack.ql.session.Configuration;
@@ -28,12 +29,12 @@ import java.util.List;
 import java.util.Locale;
 import java.util.function.Function;
 
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isStringAndExact;
 import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER;
 import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToString;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.FIRST;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.SECOND;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isDate;
-import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
 
 public class DateFormat extends EsqlConfigurationFunction implements OptionalArgument {
 
@@ -43,7 +44,7 @@ public class DateFormat extends EsqlConfigurationFunction implements OptionalArg
     @FunctionInfo(returnType = "keyword", description = "Returns a string representation of a date, in the provided format.")
     public DateFormat(
         Source source,
-        @Param(optional = true, name = "dateFormat", type = { "keyword" }, description = "A valid date pattern") Expression format,
+        @Param(optional = true, name = "dateFormat", type = { "keyword", "text" }, description = "A valid date pattern") Expression format,
         @Param(name = "date", type = { "date" }, description = "Date expression") Expression date,
         Configuration configuration
     ) {
@@ -96,23 +97,17 @@ public class DateFormat extends EsqlConfigurationFunction implements OptionalArg
     public ExpressionEvaluator.Factory toEvaluator(Function<Expression, ExpressionEvaluator.Factory> toEvaluator) {
         var fieldEvaluator = toEvaluator.apply(field);
         if (format == null) {
-            return dvrCtx -> new DateFormatConstantEvaluator(source(), fieldEvaluator.get(dvrCtx), DEFAULT_DATE_TIME_FORMATTER, dvrCtx);
+            return new DateFormatConstantEvaluator.Factory(source(), fieldEvaluator, DEFAULT_DATE_TIME_FORMATTER);
         }
-        if (format.dataType() != DataTypes.KEYWORD) {
+        if (EsqlDataTypes.isString(format.dataType()) == false) {
             throw new IllegalArgumentException("unsupported data type for format [" + format.dataType() + "]");
         }
         if (format.foldable()) {
             DateFormatter formatter = toFormatter(format.fold(), ((EsqlConfiguration) configuration()).locale());
-            return dvrCtx -> new DateFormatConstantEvaluator(source(), fieldEvaluator.get(dvrCtx), formatter, dvrCtx);
+            return new DateFormatConstantEvaluator.Factory(source(), fieldEvaluator, formatter);
         }
         var formatEvaluator = toEvaluator.apply(format);
-        return dvrCtx -> new DateFormatEvaluator(
-            source(),
-            fieldEvaluator.get(dvrCtx),
-            formatEvaluator.get(dvrCtx),
-            ((EsqlConfiguration) configuration()).locale(),
-            dvrCtx
-        );
+        return new DateFormatEvaluator.Factory(source(), fieldEvaluator, formatEvaluator, ((EsqlConfiguration) configuration()).locale());
     }
 
     private static DateFormatter toFormatter(Object format, Locale locale) {

+ 4 - 3
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java

@@ -15,6 +15,7 @@ import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator;
 import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
 import org.elasticsearch.xpack.esql.expression.function.Param;
 import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction;
+import org.elasticsearch.xpack.esql.type.EsqlDataTypes;
 import org.elasticsearch.xpack.ql.InvalidArgumentException;
 import org.elasticsearch.xpack.ql.expression.Expression;
 import org.elasticsearch.xpack.ql.expression.function.OptionalArgument;
@@ -28,12 +29,12 @@ import java.util.List;
 import java.util.function.Function;
 
 import static org.elasticsearch.common.time.DateFormatter.forPattern;
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isStringAndExact;
 import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER;
 import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToLong;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.FIRST;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.SECOND;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
-import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
 import static org.elasticsearch.xpack.ql.util.DateUtils.UTC;
 
 public class DateParse extends EsqlScalarFunction implements OptionalArgument {
@@ -44,7 +45,7 @@ public class DateParse extends EsqlScalarFunction implements OptionalArgument {
     @FunctionInfo(returnType = "date", description = "Parses a string into a date value")
     public DateParse(
         Source source,
-        @Param(name = "datePattern", type = { "keyword" }, description = "A valid date pattern", optional = true) Expression first,
+        @Param(name = "datePattern", type = { "keyword", "text" }, description = "A valid date pattern", optional = true) Expression first,
         @Param(name = "dateString", type = { "keyword", "text" }, description = "A string representing a date") Expression second
     ) {
         super(source, second != null ? List.of(first, second) : List.of(first));
@@ -99,7 +100,7 @@ public class DateParse extends EsqlScalarFunction implements OptionalArgument {
         if (format == null) {
             return new DateParseConstantEvaluator.Factory(source(), fieldEvaluator, DEFAULT_DATE_TIME_FORMATTER);
         }
-        if (format.dataType() != DataTypes.KEYWORD) {
+        if (EsqlDataTypes.isString(format.dataType()) == false) {
             throw new IllegalArgumentException("unsupported data type for date_parse [" + format.dataType() + "]");
         }
         if (format.foldable()) {

+ 5 - 6
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ip/CIDRMatch.java

@@ -28,10 +28,10 @@ import java.util.List;
 import java.util.function.Function;
 
 import static java.util.Collections.singletonList;
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isStringAndExact;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.FIRST;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.fromIndex;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isIPAndExact;
-import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
 
 /**
  * This function takes a first parameter of type IP, followed by one or more parameters evaluated to a CIDR specification:
@@ -53,7 +53,7 @@ public class CIDRMatch extends EsqlScalarFunction {
     public CIDRMatch(
         Source source,
         @Param(name = "ip", type = { "ip" }) Expression ipField,
-        @Param(name = "blockX", type = { "keyword" }, description = "CIDR block to test the IP against.") List<Expression> matches
+        @Param(name = "blockX", type = { "keyword", "text" }, description = "CIDR block to test the IP against.") List<Expression> matches
     ) {
         super(source, CollectionUtils.combine(singletonList(ipField), matches));
         this.ipField = ipField;
@@ -76,11 +76,10 @@ public class CIDRMatch extends EsqlScalarFunction {
     @Override
     public ExpressionEvaluator.Factory toEvaluator(Function<Expression, ExpressionEvaluator.Factory> toEvaluator) {
         var ipEvaluatorSupplier = toEvaluator.apply(ipField);
-        return dvrCtx -> new CIDRMatchEvaluator(
+        return new CIDRMatchEvaluator.Factory(
             source(),
-            ipEvaluatorSupplier.get(dvrCtx),
-            matches.stream().map(x -> toEvaluator.apply(x).get(dvrCtx)).toArray(EvalOperator.ExpressionEvaluator[]::new),
-            dvrCtx
+            ipEvaluatorSupplier,
+            matches.stream().map(x -> toEvaluator.apply(x)).toArray(EvalOperator.ExpressionEvaluator.Factory[]::new)
         );
     }
 

+ 2 - 3
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Split.java

@@ -25,10 +25,9 @@ import org.elasticsearch.xpack.ql.type.DataTypes;
 
 import java.util.function.Function;
 
+import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isStringAndExact;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.FIRST;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.SECOND;
-import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
-import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
 
 /**
  * Splits a string on some delimiter into a multivalued string field.
@@ -59,7 +58,7 @@ public class Split extends BinaryScalarFunction implements EvaluatorMapper {
             return resolution;
         }
 
-        return isString(right(), sourceText(), SECOND);
+        return isStringAndExact(right(), sourceText(), SECOND);
     }
 
     @Override

+ 1 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java

@@ -896,6 +896,7 @@ public abstract class AbstractFunctionTestCase extends ESTestCase {
         ),
         Map.entry(Set.of(DataTypes.DOUBLE, DataTypes.NULL), "double"),
         Map.entry(Set.of(DataTypes.INTEGER, DataTypes.NULL), "integer"),
+        Map.entry(Set.of(DataTypes.IP, DataTypes.NULL), "ip"),
         Map.entry(Set.of(DataTypes.LONG, DataTypes.INTEGER, DataTypes.UNSIGNED_LONG, DataTypes.DOUBLE, DataTypes.NULL), "numeric"),
         Map.entry(Set.of(DataTypes.KEYWORD, DataTypes.TEXT, DataTypes.VERSION, DataTypes.NULL), "string or version"),
         Map.entry(Set.of(DataTypes.KEYWORD, DataTypes.TEXT, DataTypes.NULL), "string"),

+ 3 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractScalarFunctionTestCase.java

@@ -177,6 +177,9 @@ public abstract class AbstractScalarFunctionTestCase extends AbstractFunctionTes
         if (withoutNull.equals(List.of(DataTypes.DATETIME))) {
             return "datetime";
         }
+        if (withoutNull.equals(List.of(DataTypes.IP))) {
+            return "ip";
+        }
         List<DataType> negations = Stream.concat(Stream.of(numerics()), Stream.of(EsqlDataTypes.DATE_PERIOD, EsqlDataTypes.TIME_DURATION))
             .sorted(Comparator.comparing(DataType::name))
             .toList();

+ 12 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java

@@ -53,6 +53,18 @@ public class DateExtractTests extends AbstractScalarFunctionTestCase {
                         equalTo(2023L)
                     )
                 ),
+                new TestCaseSupplier(
+                    List.of(DataTypes.TEXT, DataTypes.DATETIME),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(
+                            new TestCaseSupplier.TypedData(new BytesRef("YeAr"), DataTypes.TEXT, "chrono"),
+                            new TestCaseSupplier.TypedData(1687944333000L, DataTypes.DATETIME, "date")
+                        ),
+                        "DateExtractEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]",
+                        DataTypes.LONG,
+                        equalTo(2023L)
+                    )
+                ),
                 new TestCaseSupplier(
                     List.of(DataTypes.KEYWORD, DataTypes.DATETIME),
                     () -> new TestCaseSupplier.TestCase(

+ 79 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java

@@ -0,0 +1,79 @@
+/*
+ * 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.date;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.lucene.BytesRefs;
+import org.elasticsearch.xpack.esql.EsqlTestUtils;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class DateFormatTests extends AbstractScalarFunctionTestCase {
+    public DateFormatTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+        return parameterSuppliersFromTypedData(
+            List.of(
+                new TestCaseSupplier(
+                    List.of(DataTypes.KEYWORD, DataTypes.DATETIME),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(
+                            new TestCaseSupplier.TypedData(new BytesRef("yyyy"), DataTypes.KEYWORD, "formatter"),
+                            new TestCaseSupplier.TypedData(1687944333000L, DataTypes.DATETIME, "val")
+                        ),
+                        "DateFormatEvaluator[val=Attribute[channel=1], formatter=Attribute[channel=0], locale=en_US]",
+                        DataTypes.KEYWORD,
+                        equalTo(BytesRefs.toBytesRef("2023"))
+                    )
+                ),
+                new TestCaseSupplier(
+                    List.of(DataTypes.TEXT, DataTypes.DATETIME),
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(
+                            new TestCaseSupplier.TypedData(new BytesRef("yyyy"), DataTypes.TEXT, "formatter"),
+                            new TestCaseSupplier.TypedData(1687944333000L, DataTypes.DATETIME, "val")
+                        ),
+                        "DateFormatEvaluator[val=Attribute[channel=1], formatter=Attribute[channel=0], locale=en_US]",
+                        DataTypes.KEYWORD,
+                        equalTo(BytesRefs.toBytesRef("2023"))
+                    )
+                )
+            )
+        );
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new DateFormat(source, args.get(0), args.get(1), EsqlTestUtils.TEST_CFG);
+    }
+
+    @Override
+    protected List<ArgumentSpec> argSpec() {
+        return List.of(required(strings()), required(DataTypes.DATETIME));
+    }
+
+    @Override
+    protected DataType expectedType(List<DataType> argTypes) {
+        return DataTypes.KEYWORD;
+    }
+}

+ 12 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseTests.java

@@ -62,6 +62,18 @@ public class DateParseTests extends AbstractScalarFunctionTestCase {
                         equalTo(1683244800000L)
                     )
                 ),
+                new TestCaseSupplier(
+                    "With Both Text",
+                    () -> new TestCaseSupplier.TestCase(
+                        List.of(
+                            new TestCaseSupplier.TypedData(new BytesRef("yyyy-MM-dd"), DataTypes.TEXT, "second"),
+                            new TestCaseSupplier.TypedData(new BytesRef("2023-05-05"), DataTypes.TEXT, "first")
+                        ),
+                        "DateParseEvaluator[val=Attribute[channel=1], formatter=Attribute[channel=0], zoneId=Z]",
+                        DataTypes.DATETIME,
+                        equalTo(1683244800000L)
+                    )
+                ),
                 new TestCaseSupplier(
                     List.of(DataTypes.KEYWORD, DataTypes.KEYWORD),
                     () -> new TestCaseSupplier.TestCase(

+ 104 - 0
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/ip/CIDRMatchTests.java

@@ -0,0 +1,104 @@
+/*
+ * 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.ip;
+
+import com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.lucene.BytesRefs;
+import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
+import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractScalarFunctionTestCase;
+import org.elasticsearch.xpack.ql.expression.Expression;
+import org.elasticsearch.xpack.ql.tree.Source;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.EsqlConverter.STRING_TO_IP;
+import static org.hamcrest.Matchers.equalTo;
+
+public class CIDRMatchTests extends AbstractScalarFunctionTestCase {
+    public CIDRMatchTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
+        this.testCase = testCaseSupplier.get();
+    }
+
+    @ParametersFactory
+    public static Iterable<Object[]> parameters() {
+
+        var suppliers = List.of(
+            new TestCaseSupplier(
+                List.of(DataTypes.IP, DataTypes.KEYWORD),
+                () -> new TestCaseSupplier.TestCase(
+                    List.of(
+                        new TestCaseSupplier.TypedData(STRING_TO_IP.convert(BytesRefs.toBytesRef("192.168.0.10")), DataTypes.IP, "ip"),
+                        new TestCaseSupplier.TypedData(new BytesRef("192.168.0.0/16"), DataTypes.KEYWORD, "cidrs")
+                    ),
+                    "CIDRMatchEvaluator[ip=Attribute[channel=0], cidrs=[Attribute[channel=1]]]",
+                    DataTypes.BOOLEAN,
+                    equalTo(true)
+                )
+            ),
+            new TestCaseSupplier(
+                List.of(DataTypes.IP, DataTypes.TEXT),
+                () -> new TestCaseSupplier.TestCase(
+                    List.of(
+                        new TestCaseSupplier.TypedData(STRING_TO_IP.convert(BytesRefs.toBytesRef("192.168.0.10")), DataTypes.IP, "ip"),
+                        new TestCaseSupplier.TypedData(new BytesRef("192.168.0.0/16"), DataTypes.TEXT, "cidrs")
+                    ),
+                    "CIDRMatchEvaluator[ip=Attribute[channel=0], cidrs=[Attribute[channel=1]]]",
+                    DataTypes.BOOLEAN,
+                    equalTo(true)
+                )
+            ),
+            new TestCaseSupplier(
+                List.of(DataTypes.IP, DataTypes.KEYWORD),
+                () -> new TestCaseSupplier.TestCase(
+                    List.of(
+                        new TestCaseSupplier.TypedData(STRING_TO_IP.convert(BytesRefs.toBytesRef("192.168.0.10")), DataTypes.IP, "ip"),
+                        new TestCaseSupplier.TypedData(new BytesRef("10.0.0.0/16"), DataTypes.KEYWORD, "cidrs")
+                    ),
+                    "CIDRMatchEvaluator[ip=Attribute[channel=0], cidrs=[Attribute[channel=1]]]",
+                    DataTypes.BOOLEAN,
+                    equalTo(false)
+                )
+            ),
+            new TestCaseSupplier(
+                List.of(DataTypes.IP, DataTypes.TEXT),
+                () -> new TestCaseSupplier.TestCase(
+                    List.of(
+                        new TestCaseSupplier.TypedData(STRING_TO_IP.convert(BytesRefs.toBytesRef("192.168.0.10")), DataTypes.IP, "ip"),
+                        new TestCaseSupplier.TypedData(new BytesRef("10.0.0.0/16"), DataTypes.TEXT, "cidrs")
+                    ),
+                    "CIDRMatchEvaluator[ip=Attribute[channel=0], cidrs=[Attribute[channel=1]]]",
+                    DataTypes.BOOLEAN,
+                    equalTo(false)
+                )
+            )
+        );
+
+        return parameterSuppliersFromTypedData(errorsForCasesWithoutExamples(anyNullIsNull(true, suppliers)));
+    }
+
+    @Override
+    protected Expression build(Source source, List<Expression> args) {
+        return new CIDRMatch(source, args.get(0), List.of(args.get(1)));
+    }
+
+    @Override
+    protected List<ArgumentSpec> argSpec() {
+        return List.of(required(DataTypes.IP), required(strings()));
+    }
+
+    @Override
+    protected DataType expectedType(List<DataType> argTypes) {
+        return DataTypes.BOOLEAN;
+    }
+}

+ 61 - 0
x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/100_bug_fix.yml

@@ -193,3 +193,64 @@
   - match: { columns.0.type: double }
   - length: { values: 1 }
   - match: { values.0.0: 30.0 }
+
+
+
+---
+"text in functions #105379":
+  - skip:
+      version: " - 8.13.99"
+      reason: "fixes in 8.13 or later"
+  - do:
+      indices.create:
+        index: idx_with_date_ip_txt
+        body:
+          mappings:
+            properties:
+              id:
+                type: long
+              date:
+                type: date
+              ip:
+                type: ip
+              text:
+                type: text
+              text2:
+                type: text
+
+  - do:
+      bulk:
+        refresh: true
+        body:
+          - { "index": { "_index": "idx_with_date_ip_txt" } }
+          - { "id": 1, "date": "2024-03-22T14:50:00.000Z", "ip": "192.168.0.10", "text":"yyyy-MM-dd", "text2":"year" }
+          - { "index": { "_index": "idx_with_date_ip_txt" } }
+          - { "id": 2, "date": "2024-03-22T14:50:00.000Z", "ip": "192.168.0.10", "text": "192.168.0.0/16" }
+          - { "index": { "_index": "idx_with_date_ip_txt" } }
+          - { "id": 3, "date": "2024-03-22T14:50:00.000Z", "ip": "10.0.0.10", "text": "192.168.0.0/16" }
+  - do:
+      esql.query:
+        body:
+          query: 'from idx_with_date_ip_txt | where id == 1 | eval x = date_format(text, date), y = date_extract(text2, date), p = date_parse(text, "2024-03-14") | keep x, y, p | limit 1'
+  - match: { columns.0.name: x }
+  - match: { columns.0.type: keyword }
+  - match: { columns.1.name: y }
+  - match: { columns.1.type: long }
+  - length: { values: 1 }
+  - match: { values.0.0: "2024-03-22" }
+  - match: { values.0.1: 2024 }
+  - match: { values.0.2: "2024-03-14T00:00:00.000Z" }
+
+  - do:
+      esql.query:
+        body:
+          query: 'from idx_with_date_ip_txt | where id > 1 | eval x = cidr_match(ip, text) | sort id | keep id, x | limit 2'
+  - match: { columns.0.name: id }
+  - match: { columns.0.type: long }
+  - match: { columns.1.name: x }
+  - match: { columns.1.type: boolean }
+  - length: { values: 2 }
+  - match: { values.0.0: 2 }
+  - match: { values.0.1: true }
+  - match: { values.1.0: 3 }
+  - match: { values.1.1: false }

+ 20 - 0
x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/80_text.yml

@@ -366,6 +366,26 @@ setup:
   - match: { values.0: [ "Jenny - IT Director"] }
   - match: { values.1: [ "John - Payroll Specialist"] }
 
+---
+"split text":
+  - skip:
+      version: " - 8.13.99"
+      reason: "functions fixed for text in v 8.14"
+      features: allowed_warnings_regex
+  - do:
+      allowed_warnings_regex:
+        - "No limit defined, adding default limit of \\[.*\\]"
+      esql.query:
+        body:
+          query: 'from test | sort emp_no | eval split = split(tag, " ") | keep split'
+
+  - match: { columns.0.name: "split" }
+  - match: { columns.0.type: "keyword" }
+
+  - length: { values: 2 }
+  - match: { values.0: [ ["foo", "bar"] ] }
+  - match: { values.1: [ "baz"] }
+
 
 ---
 "stats text with raw":