Browse Source

ESQL: Swap arguments of remaining date_xxx() functions (#99561)

This swaps the argument of `date_extract()`, `date_format()` and
`date_parse()` functions, to align with `date_trunc()`. The field
argument is now always last, even for _format() and _parse(), whose
optional argument will now be provided as the first one.
Bogdan Pintea 2 years ago
parent
commit
34eea49ef5
19 changed files with 133 additions and 93 deletions
  1. 1 1
      docs/reference/esql/functions/date_format.asciidoc
  2. 1 1
      docs/reference/esql/functions/types/date_extract.asciidoc
  3. 2 2
      x-pack/plugin/esql/qa/server/single-node/src/yamlRestTest/resources/rest-api-spec/test/70_locale.yml
  4. 1 1
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/blog-ignoreCsvTests.csv-spec
  5. 19 19
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec
  6. 4 4
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/docs.csv-spec
  7. 1 1
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec
  8. 1 1
      x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_count_distinct.csv-spec
  9. 1 1
      x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionRuntimeFieldIT.java
  10. 11 0
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/BinaryDateTimeFunction.java
  11. 19 9
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtract.java
  12. 18 8
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormat.java
  13. 9 8
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java
  14. 6 12
      x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java
  15. 23 9
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java
  16. 5 5
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java
  17. 4 4
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseTests.java
  18. 2 2
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/EvalMapperTests.java
  19. 5 5
      x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java

+ 1 - 1
docs/reference/esql/functions/date_format.asciidoc

@@ -7,5 +7,5 @@ is specified, the `yyyy-MM-dd'T'HH:mm:ss.SSSZ` format is used.
 ----
 FROM employees
 | KEEP first_name, last_name, hire_date
-| EVAL hired = DATE_FORMAT(hire_date, "YYYY-MM-dd")
+| EVAL hired = DATE_FORMAT("YYYY-MM-dd", hire_date)
 ----

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

@@ -1,5 +1,5 @@
 [%header.monospaced.styled,format=dsv,separator=|]
 |===
 arg1 | arg2 | result
-datetime | keyword | long
+keyword | datetime | long
 |===

+ 2 - 2
x-pack/plugin/esql/qa/server/single-node/src/yamlRestTest/resources/rest-api-spec/test/70_locale.yml

@@ -26,7 +26,7 @@ setup:
   - do:
       esql.query:
         body:
-          query: 'FROM events | eval fixed_format = date_format(@timestamp, "MMMM"), variable_format = date_format(@timestamp, format) | sort @timestamp | keep @timestamp, fixed_format, variable_format'
+          query: 'FROM events | eval fixed_format = date_format("MMMM", @timestamp), variable_format = date_format(format, @timestamp) | sort @timestamp | keep @timestamp, fixed_format, variable_format'
 
   - match: { columns.0.name: "@timestamp" }
   - match: { columns.0.type: "date" }
@@ -45,7 +45,7 @@ setup:
   - do:
       esql.query:
         body:
-          query: 'FROM events | eval fixed_format = date_format(@timestamp, "MMMM"), variable_format = date_format(@timestamp, format) | sort @timestamp | keep @timestamp, fixed_format, variable_format'
+          query: 'FROM events | eval fixed_format = date_format("MMMM", @timestamp), variable_format = date_format(format, @timestamp) | sort @timestamp | keep @timestamp, fixed_format, variable_format'
           locale: "it-IT"
 
   - match: { columns.0.name: "@timestamp" }

+ 1 - 1
x-pack/plugin/esql/qa/testFixtures/src/main/resources/blog-ignoreCsvTests.csv-spec

@@ -2,7 +2,7 @@
 
   FROM employees
 | WHERE still_hired == true
-| EVAL hired = DATE_FORMAT(hire_date, "YYYY")
+| EVAL hired = DATE_FORMAT("YYYY", hire_date)
 | STATS avg_salary = AVG(salary) BY languages
 | EVAL avg_salary = ROUND(avg_salary)
 | EVAL lang_code = TO_STRING(languages)

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

@@ -45,7 +45,7 @@ emp_no:integer | x:date
 
 
 evalDateFormat
-from employees | sort hire_date | eval x = date_format(hire_date), y = date_format(hire_date, "YYYY-MM-dd") | keep emp_no, x, y | limit 5;
+from employees | sort hire_date | eval x = date_format(hire_date), y = date_format("YYYY-MM-dd", hire_date) | keep emp_no, x, y | limit 5;
 
 emp_no:integer | x:keyword                     | y:keyword
 10009          | 1985-02-18T00:00:00.000Z      | 1985-02-18            
@@ -295,7 +295,7 @@ hire_date:date           | hd:date
 ;
 
 now
-row a = now() | eval x = a == now(), y = substring(date_format(a, "yyyy"), 0, 2) | keep x, y;
+row a = now() | eval x = a == now(), y = substring(date_format("yyyy", a), 0, 2) | keep x, y;
 
 x:boolean  | y:keyword
 true       | 20
@@ -338,14 +338,14 @@ AVG(salary):double | bucket:date
 ;
 
 evalDateParseWithSimpleDate
-row a = "2023-02-01" | eval b = date_parse(a, "yyyy-MM-dd") | keep b;
+row a = "2023-02-01" | eval b = date_parse("yyyy-MM-dd", a) | keep b;
 
 b:datetime
 2023-02-01T00:00:00.000Z
 ;
 
 evalDateParseWithDateTime
-row a = "2023-02-01 12:15:55" | eval b = date_parse(a, "yyyy-MM-dd HH:mm:ss") | keep b;
+row a = "2023-02-01 12:15:55" | eval b = date_parse("yyyy-MM-dd HH:mm:ss", a) | keep b;
 
 b:datetime
 2023-02-01T12:15:55.000Z
@@ -359,8 +359,8 @@ b:datetime
 ;
 
 evalDateParseWrongDate
-row a = "2023-02-01 foo" | eval b = date_parse(a, "yyyy-MM-dd") | keep b;
-warning:Line 1:37: evaluation of [date_parse(a, \"yyyy-MM-dd\")] failed, treating result as null. Only first 20 failures recorded.
+row a = "2023-02-01 foo" | eval b = date_parse("yyyy-MM-dd", a) | keep b;
+warning:Line 1:37: evaluation of [date_parse(\"yyyy-MM-dd\", a)] failed, treating result as null. Only first 20 failures recorded.
 warning:java.lang.IllegalArgumentException: failed to parse date field [2023-02-01 foo] with format [yyyy-MM-dd]
 
 b:datetime
@@ -368,16 +368,16 @@ null
 ;
 
 evalDateParseNotMatching
-row a = "2023-02-01" | eval b = date_parse(a, "yyyy-MM") | keep b;
-warning:Line 1:33: evaluation of [date_parse(a, \"yyyy-MM\")] failed, treating result as null. Only first 20 failures recorded.
+row a = "2023-02-01" | eval b = date_parse("yyyy-MM", a) | keep b;
+warning:Line 1:33: evaluation of [date_parse(\"yyyy-MM\", a)] failed, treating result as null. Only first 20 failures recorded.
 warning:java.lang.IllegalArgumentException: failed to parse date field [2023-02-01] with format [yyyy-MM]
 b:datetime
 null
 ;
 
 evalDateParseNotMatching2
-row a = "2023-02-01" | eval b = date_parse(a, "yyyy-MM-dd HH:mm:ss") | keep b;
-warning:Line 1:33: evaluation of [date_parse(a, \"yyyy-MM-dd HH:mm:ss\")] failed, treating result as null. Only first 20 failures recorded.
+row a = "2023-02-01" | eval b = date_parse("yyyy-MM-dd HH:mm:ss", a) | keep b;
+warning:Line 1:33: evaluation of [date_parse(\"yyyy-MM-dd HH:mm:ss\", a)] failed, treating result as null. Only first 20 failures recorded.
 warning:java.lang.IllegalArgumentException: failed to parse date field [2023-02-01] with format [yyyy-MM-dd HH:mm:ss]
 
 b:datetime
@@ -385,7 +385,7 @@ null
 ;
 
 evalDateParseNullPattern
-row a = "2023-02-01" | eval b = date_parse(a, null) | keep b;
+row a = "2023-02-01" | eval b = date_parse(null, a) | keep b;
 
 b:datetime
 null
@@ -393,8 +393,8 @@ null
 
 evalDateParseDynamic
 from employees | where emp_no == 10039 or emp_no == 10040 | sort emp_no 
-| eval birth_date_string = date_format(birth_date, "yyyy-MM-dd")
-| eval new_date = date_parse(birth_date_string, "yyyy-MM-dd") | eval bool = new_date == birth_date | keep emp_no, new_date, birth_date, bool;
+| eval birth_date_string = date_format("yyyy-MM-dd", birth_date)
+| eval new_date = date_parse("yyyy-MM-dd", birth_date_string) | eval bool = new_date == birth_date | keep emp_no, new_date, birth_date, bool;
 
 emp_no:integer  | new_date:datetime | birth_date:datetime | bool:boolean
 10039           | 1959-10-01        | 1959-10-01          | true
@@ -403,8 +403,8 @@ emp_no:integer  | new_date:datetime | birth_date:datetime | bool:boolean
 
 evalDateParseDynamic2
 from employees | where emp_no >= 10047 | sort emp_no | where emp_no <= 10051 
-| eval birth_date_string = date_format(birth_date, "yyyy-MM-dd") 
-| eval new_date = date_parse(birth_date_string, "yyyy-MM-dd") 
+| eval birth_date_string = date_format("yyyy-MM-dd", birth_date) 
+| eval new_date = date_parse("yyyy-MM-dd", birth_date_string) 
 | keep emp_no, new_date, birth_date | eval bool = new_date == birth_date;
 
 emp_no:integer | new_date:datetime        | birth_date:datetime       | bool:boolean      
@@ -418,8 +418,8 @@ emp_no:integer | new_date:datetime        | birth_date:datetime       | bool:boo
 
 evalDateParseDynamicDateAndPattern
 from employees | where emp_no == 10049 or emp_no == 10050 | sort emp_no 
-| eval pattern = "yyyy-MM-dd", birth_date_string = date_format(birth_date, pattern)
-| eval new_date = date_parse(birth_date_string, "yyyy-MM-dd") | eval bool = new_date == birth_date | keep emp_no, new_date, birth_date, bool;
+| eval pattern = "yyyy-MM-dd", birth_date_string = date_format(pattern, birth_date)
+| eval new_date = date_parse("yyyy-MM-dd", birth_date_string) | eval bool = new_date == birth_date | keep emp_no, new_date, birth_date, bool;
 
 emp_no:integer  | new_date:datetime | birth_date:datetime | bool:boolean
 10049           | null              | null                | null
@@ -437,7 +437,7 @@ emp_no:integer  | new_date:datetime          | birth_date:datetime       | bool:
 
 dateFields
 from employees | where emp_no == 10049 or emp_no == 10050 
-| eval year = date_extract(birth_date, "year"), month = date_extract(birth_date, "month_of_year"), day = date_extract(birth_date, "day_of_month")
+| eval year = date_extract("year", birth_date), month = date_extract("month_of_year", birth_date), day = date_extract("day_of_month", birth_date)
 | keep emp_no, year, month, day;
 ignoreOrder:true
 
@@ -449,7 +449,7 @@ emp_no:integer | year:long    | month:long    | day:long
 
 dateFormatLocale
 from employees | where emp_no == 10049 or emp_no == 10050 | sort emp_no 
-| eval birth_month = date_format(birth_date, "MMMM") | keep emp_no, birth_date, birth_month;
+| eval birth_month = date_format("MMMM", birth_date) | keep emp_no, birth_date, birth_month;
 ignoreOrder:true
 
 emp_no:integer  |  birth_date:datetime       | birth_month:keyword

+ 4 - 4
x-pack/plugin/esql/qa/testFixtures/src/main/resources/docs.csv-spec

@@ -233,7 +233,7 @@ avg_lang:double | max_lang:integer
 docsStatsGroupByMultipleValues
 // tag::statsGroupByMultipleValues[]
 FROM employees
-| EVAL hired = DATE_FORMAT(hire_date, "YYYY")
+| EVAL hired = DATE_FORMAT("YYYY", hire_date)
 | STATS avg_salary = AVG(salary) BY hired, languages.long
 | EVAL avg_salary = ROUND(avg_salary)
 | SORT hired, languages.long
@@ -293,8 +293,8 @@ Uri            |Lenart         |1.75
 
 dateExtract
 // tag::dateExtract[]
-ROW date = DATE_PARSE("2022-05-06", "yyyy-MM-dd")
-| EVAL year = DATE_EXTRACT(date, "year")
+ROW date = DATE_PARSE("yyyy-MM-dd", "2022-05-06")
+| EVAL year = DATE_EXTRACT("year", date)
 // end::dateExtract[]
 ;
 
@@ -404,7 +404,7 @@ Saniya         |Kalloufi       |2.1          |6.9
 dateParse
 // tag::dateParse[]
 ROW date_string = "2022-05-06"
-| EVAL date = DATE_PARSE(date_string, "yyyy-MM-dd")
+| EVAL date = DATE_PARSE("yyyy-MM-dd", date_string)
 // end::dateParse[]
 ;
 

+ 1 - 1
x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec

@@ -322,7 +322,7 @@ c:long | g:keyword | tws:long
 ;
 
 byStringAndString
-from employees | eval hire_year_str = date_format(hire_date, "yyyy") | stats c = count(gender) by gender, hire_year_str | sort c desc, gender, hire_year_str | where c >= 5;
+from employees | eval hire_year_str = date_format("yyyy", hire_date) | stats c = count(gender) by gender, hire_year_str | sort c desc, gender, hire_year_str | where c >= 5;
 
 c:long | gender:keyword | hire_year_str:keyword
 8 | F | 1989

+ 1 - 1
x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_count_distinct.csv-spec

@@ -68,7 +68,7 @@ c:long
 ;
 
 countDistinctOfKeywords
-from employees | eval hire_year_str = date_format(hire_date, "yyyy") | stats g = count_distinct(gender), h = count_distinct(hire_year_str);
+from employees | eval hire_year_str = date_format("yyyy", hire_date) | stats g = count_distinct(gender), h = count_distinct(hire_year_str);
 
 g:long | h:long
 2      | 14

+ 1 - 1
x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionRuntimeFieldIT.java

@@ -88,7 +88,7 @@ public class EsqlActionRuntimeFieldIT extends AbstractEsqlIntegTestCase {
     public void testDate() throws InterruptedException, IOException {
         createIndexWithConstRuntimeField("date");
         EsqlQueryResponse response = run("""
-            from test | eval d=date_format(const, "yyyy") | stats min (foo) by d""");
+            from test | eval d=date_format("yyyy", const) | stats min (foo) by d""");
         assertThat(getValuesList(response), equalTo(List.of(List.of(0L, "2023"))));
     }
 

+ 11 - 0
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/BinaryDateTimeFunction.java

@@ -17,6 +17,9 @@ import org.elasticsearch.xpack.ql.type.DataTypes;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.util.Objects;
+import java.util.function.Predicate;
+
+import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
 
 public abstract class BinaryDateTimeFunction extends BinaryScalarFunction {
 
@@ -66,4 +69,12 @@ public abstract class BinaryDateTimeFunction extends BinaryScalarFunction {
         BinaryDateTimeFunction that = (BinaryDateTimeFunction) o;
         return zoneId().equals(that.zoneId());
     }
+
+    // TODO: drop check once 8.11 is released
+    static TypeResolution argumentTypesAreSwapped(DataType left, DataType right, Predicate<DataType> rightTest, String source) {
+        if (DataTypes.isDateTime(left) && rightTest.test(right)) {
+            return new TypeResolution(format(null, "function definition has been updated, please swap arguments in [{}]", source));
+        }
+        return TypeResolution.TYPE_RESOLVED;
+    }
 }

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

@@ -31,6 +31,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.function.Function;
 
+import static org.elasticsearch.xpack.esql.expression.function.scalar.date.BinaryDateTimeFunction.argumentTypesAreSwapped;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isDate;
 import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isStringAndExact;
 
@@ -38,22 +39,22 @@ public class DateExtract extends ConfigurationFunction implements EvaluatorMappe
 
     private ChronoField chronoField;
 
-    public DateExtract(Source source, Expression field, Expression chronoFieldExp, Configuration configuration) {
-        super(source, List.of(field, chronoFieldExp), configuration);
+    public DateExtract(Source source, Expression chronoFieldExp, Expression field, Configuration configuration) {
+        super(source, List.of(chronoFieldExp, field), configuration);
     }
 
     @Override
     public ExpressionEvaluator.Factory toEvaluator(Function<Expression, ExpressionEvaluator.Factory> toEvaluator) {
-        var fieldEvaluator = toEvaluator.apply(children().get(0));
-        if (children().get(1).foldable()) {
+        var fieldEvaluator = toEvaluator.apply(children().get(1));
+        if (children().get(0).foldable()) {
             ChronoField chrono = chronoField();
             if (chrono == null) {
-                BytesRef field = (BytesRef) children().get(1).fold();
+                BytesRef field = (BytesRef) children().get(0).fold();
                 throw new EsqlIllegalArgumentException("invalid date field for [{}]: {}", sourceText(), field.utf8ToString());
             }
             return dvrCtx -> new DateExtractConstantEvaluator(fieldEvaluator.get(dvrCtx), chrono, configuration().zoneId(), dvrCtx);
         }
-        var chronoEvaluator = toEvaluator.apply(children().get(1));
+        var chronoEvaluator = toEvaluator.apply(children().get(0));
         return dvrCtx -> new DateExtractEvaluator(
             source(),
             fieldEvaluator.get(dvrCtx),
@@ -65,7 +66,7 @@ public class DateExtract extends ConfigurationFunction implements EvaluatorMappe
 
     private ChronoField chronoField() {
         if (chronoField == null) {
-            Expression field = children().get(1);
+            Expression field = children().get(0);
             if (field.foldable() && field.dataType() == DataTypes.KEYWORD) {
                 try {
                     BytesRef br = BytesRefs.toBytesRef(field.fold());
@@ -114,11 +115,20 @@ public class DateExtract extends ConfigurationFunction implements EvaluatorMappe
         if (childrenResolved() == false) {
             return new TypeResolution("Unresolved children");
         }
-        TypeResolution resolution = isDate(children().get(0), sourceText(), TypeResolutions.ParamOrdinal.FIRST);
+        TypeResolution resolution = argumentTypesAreSwapped(
+            children().get(0).dataType(),
+            children().get(1).dataType(),
+            DataTypes::isString,
+            sourceText()
+        );
+        if (resolution.unresolved()) {
+            return resolution;
+        }
+        resolution = isStringAndExact(children().get(0), sourceText(), TypeResolutions.ParamOrdinal.FIRST);
         if (resolution.unresolved()) {
             return resolution;
         }
-        resolution = isStringAndExact(children().get(1), sourceText(), TypeResolutions.ParamOrdinal.SECOND);
+        resolution = isDate(children().get(1), sourceText(), TypeResolutions.ParamOrdinal.SECOND);
         if (resolution.unresolved()) {
             return resolution;
         }

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

@@ -24,11 +24,11 @@ import org.elasticsearch.xpack.ql.tree.Source;
 import org.elasticsearch.xpack.ql.type.DataType;
 import org.elasticsearch.xpack.ql.type.DataTypes;
 
-import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
 import java.util.function.Function;
 
+import static org.elasticsearch.xpack.esql.expression.function.scalar.date.BinaryDateTimeFunction.argumentTypesAreSwapped;
 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;
@@ -40,10 +40,10 @@ public class DateFormat extends ConfigurationFunction implements OptionalArgumen
     private final Expression field;
     private final Expression format;
 
-    public DateFormat(Source source, Expression field, Expression format, Configuration configuration) {
-        super(source, format != null ? Arrays.asList(field, format) : Arrays.asList(field), configuration);
-        this.field = field;
-        this.format = format;
+    public DateFormat(Source source, Expression first, Expression second, Configuration configuration) {
+        super(source, second != null ? List.of(first, second) : List.of(first), configuration);
+        this.field = second != null ? second : first;
+        this.format = second != null ? first : null;
     }
 
     @Override
@@ -57,12 +57,20 @@ public class DateFormat extends ConfigurationFunction implements OptionalArgumen
             return new TypeResolution("Unresolved children");
         }
 
-        TypeResolution resolution = isDate(field, sourceText(), FIRST);
+        TypeResolution resolution;
+        if (format != null) {
+            resolution = argumentTypesAreSwapped(format.dataType(), field.dataType(), DataTypes::isString, sourceText());
+            if (resolution.unresolved()) {
+                return resolution;
+            }
+        }
+
+        resolution = isDate(field, sourceText(), format == null ? FIRST : SECOND);
         if (resolution.unresolved()) {
             return resolution;
         }
         if (format != null) {
-            resolution = isStringAndExact(format, sourceText(), SECOND);
+            resolution = isStringAndExact(format, sourceText(), FIRST);
             if (resolution.unresolved()) {
                 return resolution;
             }
@@ -125,7 +133,9 @@ public class DateFormat extends ConfigurationFunction implements OptionalArgumen
 
     @Override
     protected NodeInfo<? extends Expression> info() {
-        return NodeInfo.create(this, DateFormat::new, field, format, configuration());
+        Expression first = format != null ? format : field;
+        Expression second = format != null ? field : null;
+        return NodeInfo.create(this, DateFormat::new, first, second, configuration());
     }
 
     @Override

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

@@ -24,7 +24,6 @@ import org.elasticsearch.xpack.ql.type.DataType;
 import org.elasticsearch.xpack.ql.type.DataTypes;
 
 import java.time.ZoneId;
-import java.util.Arrays;
 import java.util.List;
 import java.util.function.Function;
 
@@ -41,10 +40,10 @@ public class DateParse extends ScalarFunction implements OptionalArgument, Evalu
     private final Expression field;
     private final Expression format;
 
-    public DateParse(Source source, Expression field, Expression format) {
-        super(source, format != null ? Arrays.asList(field, format) : Arrays.asList(field));
-        this.field = field;
-        this.format = format;
+    public DateParse(Source source, Expression first, Expression second) {
+        super(source, second != null ? List.of(first, second) : List.of(first));
+        this.field = second != null ? second : first;
+        this.format = second != null ? first : null;
     }
 
     @Override
@@ -58,12 +57,12 @@ public class DateParse extends ScalarFunction implements OptionalArgument, Evalu
             return new TypeResolution("Unresolved children");
         }
 
-        TypeResolution resolution = isString(field, sourceText(), FIRST);
+        TypeResolution resolution = isString(field, sourceText(), format != null ? SECOND : FIRST);
         if (resolution.unresolved()) {
             return resolution;
         }
         if (format != null) {
-            resolution = isStringAndExact(format, sourceText(), SECOND);
+            resolution = isStringAndExact(format, sourceText(), FIRST);
             if (resolution.unresolved()) {
                 return resolution;
             }
@@ -126,7 +125,9 @@ public class DateParse extends ScalarFunction implements OptionalArgument, Evalu
 
     @Override
     protected NodeInfo<? extends Expression> info() {
-        return NodeInfo.create(this, DateParse::new, field, format);
+        Expression first = format != null ? format : field;
+        Expression second = format != null ? field : null;
+        return NodeInfo.create(this, DateParse::new, first, second);
     }
 
     @Override

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

@@ -18,7 +18,6 @@ import org.elasticsearch.xpack.ql.expression.Expression;
 import org.elasticsearch.xpack.ql.expression.function.scalar.BinaryScalarFunction;
 import org.elasticsearch.xpack.ql.tree.NodeInfo;
 import org.elasticsearch.xpack.ql.tree.Source;
-import org.elasticsearch.xpack.ql.type.DataTypes;
 
 import java.time.Duration;
 import java.time.Period;
@@ -26,8 +25,6 @@ import java.time.ZoneId;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 
-import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
-import static org.elasticsearch.xpack.esql.type.EsqlDataTypes.isTemporalAmount;
 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;
@@ -45,7 +42,12 @@ public class DateTrunc extends BinaryDateTimeFunction implements EvaluatorMapper
             return new TypeResolution("Unresolved children");
         }
 
-        TypeResolution resolution = argumentTypesAreSwapped();
+        TypeResolution resolution = argumentTypesAreSwapped(
+            left().dataType(),
+            right().dataType(),
+            EsqlDataTypes::isTemporalAmount,
+            sourceText()
+        );
         if (resolution.unresolved()) {
             return resolution;
         }
@@ -58,14 +60,6 @@ public class DateTrunc extends BinaryDateTimeFunction implements EvaluatorMapper
         return isType(interval(), EsqlDataTypes::isTemporalAmount, sourceText(), SECOND, "dateperiod", "timeduration");
     }
 
-    // TODO: drop check once 8.11 is released
-    private TypeResolution argumentTypesAreSwapped() {
-        if (DataTypes.isDateTime(left().dataType()) && isTemporalAmount(right().dataType())) {
-            return new TypeResolution(format(null, "function definition has been updated, please swap arguments in [{}]", sourceText()));
-        }
-        return TypeResolution.TYPE_RESOLVED;
-    }
-
     @Override
     public Object fold() {
         return EvaluatorMapper.super.fold();

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

@@ -917,36 +917,36 @@ public class AnalyzerTests extends ESTestCase {
     public void testDateFormatWithNumericFormat() {
         verifyUnsupported("""
             from test
-            | eval date_format(date, 1)
-            """, "second argument of [date_format(date, 1)] must be [string], found value [1] type [integer]");
+            | eval date_format(1, date)
+            """, "first argument of [date_format(1, date)] must be [string], found value [1] type [integer]");
     }
 
     public void testDateFormatWithDateFormat() {
         verifyUnsupported("""
             from test
             | eval date_format(date, date)
-            """, "second argument of [date_format(date, date)] must be [string], found value [date] type [datetime]");
+            """, "first argument of [date_format(date, date)] must be [string], found value [date] type [datetime]");
     }
 
     public void testDateParseOnInt() {
         verifyUnsupported("""
             from test
-            | eval date_parse(int, keyword)
-            """, "first argument of [date_parse(int, keyword)] must be [string], found value [int] type [integer]");
+            | eval date_parse(keyword, int)
+            """, "second argument of [date_parse(keyword, int)] must be [string], found value [int] type [integer]");
     }
 
     public void testDateParseOnDate() {
         verifyUnsupported("""
             from test
-            | eval date_parse(date, keyword)
-            """, "first argument of [date_parse(date, keyword)] must be [string], found value [date] type [datetime]");
+            | eval date_parse(keyword, date)
+            """, "second argument of [date_parse(keyword, date)] must be [string], found value [date] type [datetime]");
     }
 
     public void testDateParseOnIntPattern() {
         verifyUnsupported("""
             from test
-            | eval date_parse(keyword, int)
-            """, "second argument of [date_parse(keyword, int)] must be [string], found value [int] type [integer]");
+            | eval date_parse(int, keyword)
+            """, "first argument of [date_parse(int, keyword)] must be [string], found value [int] type [integer]");
     }
 
     public void testDateTruncOnInt() {
@@ -977,6 +977,20 @@ public class AnalyzerTests extends ESTestCase {
             """, "second argument of [date_trunc(1, date)] must be [dateperiod or timeduration], found value [1] type [integer]");
     }
 
+    public void testDateExtractWithSwappedArguments() {
+        verifyUnsupported("""
+            from test
+            | eval date_extract(date, "year")
+            """, "function definition has been updated, please swap arguments in [date_extract(date, \"year\")]");
+    }
+
+    public void testDateFormatWithSwappedArguments() {
+        verifyUnsupported("""
+            from test
+            | eval date_format(date, "yyyy-MM-dd")
+            """, "function definition has been updated, please swap arguments in [date_format(date, \"yyyy-MM-dd\")]");
+    }
+
     public void testDateTruncWithSwappedArguments() {
         verifyUnsupported("""
             from test

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

@@ -39,10 +39,10 @@ public class DateExtractTests extends AbstractScalarFunctionTestCase {
         return parameterSuppliersFromTypedData(List.of(new TestCaseSupplier("Date Extract Year", () -> {
             return new TestCaseSupplier.TestCase(
                 List.of(
-                    new TestCaseSupplier.TypedData(1687944333000L, DataTypes.DATETIME, "date"),
-                    new TestCaseSupplier.TypedData(new BytesRef("YEAR"), DataTypes.KEYWORD, "field")
+                    new TestCaseSupplier.TypedData(new BytesRef("YEAR"), DataTypes.KEYWORD, "field"),
+                    new TestCaseSupplier.TypedData(1687944333000L, DataTypes.DATETIME, "date")
                 ),
-                "DateExtractEvaluator[value=Attribute[channel=0], chronoField=Attribute[channel=1], zone=Z]",
+                "DateExtractEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]",
                 DataTypes.LONG,
                 equalTo(2023L)
             );
@@ -55,8 +55,8 @@ public class DateExtractTests extends AbstractScalarFunctionTestCase {
         for (ChronoField value : ChronoField.values()) {
             DateExtract instance = new DateExtract(
                 Source.EMPTY,
-                new Literal(Source.EMPTY, epochMilli, DataTypes.DATETIME),
                 new Literal(Source.EMPTY, new BytesRef(value.name()), DataTypes.KEYWORD),
+                new Literal(Source.EMPTY, epochMilli, DataTypes.DATETIME),
                 EsqlTestUtils.TEST_CFG
             );
 
@@ -75,7 +75,7 @@ public class DateExtractTests extends AbstractScalarFunctionTestCase {
 
     @Override
     protected List<ArgumentSpec> argSpec() {
-        return List.of(required(DataTypes.DATETIME), required(strings()));
+        return List.of(required(strings()), required(DataTypes.DATETIME));
     }
 
     @Override

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

@@ -33,10 +33,10 @@ public class DateParseTests extends AbstractScalarFunctionTestCase {
         return parameterSuppliersFromTypedData(List.of(new TestCaseSupplier("Basic Case", () -> {
             return new TestCaseSupplier.TestCase(
                 List.of(
-                    new TestCaseSupplier.TypedData(new BytesRef("2023-05-05"), DataTypes.KEYWORD, "first"),
-                    new TestCaseSupplier.TypedData(new BytesRef("yyyy-MM-dd"), DataTypes.KEYWORD, "second")
+                    new TestCaseSupplier.TypedData(new BytesRef("yyyy-MM-dd"), DataTypes.KEYWORD, "second"),
+                    new TestCaseSupplier.TypedData(new BytesRef("2023-05-05"), DataTypes.KEYWORD, "first")
                 ),
-                "DateParseEvaluator[val=Attribute[channel=0], formatter=Attribute[channel=1], zoneId=Z]",
+                "DateParseEvaluator[val=Attribute[channel=1], formatter=Attribute[channel=0], zoneId=Z]",
                 DataTypes.DATETIME,
                 equalTo(1683244800000L)
             );
@@ -50,7 +50,7 @@ public class DateParseTests extends AbstractScalarFunctionTestCase {
 
     @Override
     protected List<ArgumentSpec> argSpec() {
-        return List.of(required(strings()), optional(strings()));
+        return List.of(optional(strings()), required(strings()));
     }
 
     @Override

+ 2 - 2
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/EvalMapperTests.java

@@ -103,8 +103,8 @@ public class EvalMapperTests extends ESTestCase {
             DOUBLE1,
             literal,
             new Length(Source.EMPTY, literal),
-            new DateFormat(Source.EMPTY, DATE, datePattern, TEST_CONFIG),
-            new DateFormat(Source.EMPTY, literal, datePattern, TEST_CONFIG),
+            new DateFormat(Source.EMPTY, datePattern, DATE, TEST_CONFIG),
+            new DateFormat(Source.EMPTY, datePattern, literal, TEST_CONFIG),
             new StartsWith(Source.EMPTY, literal, literal),
             new Substring(Source.EMPTY, literal, LONG, LONG),
             new DateTrunc(Source.EMPTY, dateInterval, DATE) }) {

+ 5 - 5
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/IndexResolverFieldNamesTests.java

@@ -236,8 +236,8 @@ public class IndexResolverFieldNamesTests extends ESTestCase {
             from employees
             | where emp_no == 10039 or emp_no == 10040
             | sort emp_no
-            | eval birth_date_string = date_format(birth_date, "yyyy-MM-dd")
-            | eval new_date = date_parse(birth_date_string, "yyyy-MM-dd")
+            | eval birth_date_string = date_format("yyyy-MM-dd", birth_date)
+            | eval new_date = date_parse("yyyy-MM-dd", birth_date_string)
             | eval bool = new_date == birth_date
             | keep emp_no, new_date, birth_date, bool""", Set.of("emp_no", "emp_no.*", "birth_date", "birth_date.*"));
     }
@@ -246,7 +246,7 @@ public class IndexResolverFieldNamesTests extends ESTestCase {
         assertFieldNames("""
             from employees
             | where emp_no == 10049 or emp_no == 10050
-            | eval year = date_extract(birth_date, "year"), month = date_extract(birth_date, "month_of_year")
+            | eval year = date_extract("year", birth_date), month = date_extract("month_of_year", birth_date)
             | keep emp_no, year, month""", Set.of("emp_no", "emp_no.*", "birth_date", "birth_date.*"));
     }
 
@@ -793,7 +793,7 @@ public class IndexResolverFieldNamesTests extends ESTestCase {
     public void testByStringAndString() {
         assertFieldNames("""
             from employees
-            | eval hire_year_str = date_format(hire_date, "yyyy")
+            | eval hire_year_str = date_format("yyyy", hire_date)
             | stats c = count(gender) by gender, hire_year_str
             | sort c desc, gender, hire_year_str
             | where c >= 5""", Set.of("hire_date", "hire_date.*", "gender", "gender.*"));
@@ -822,7 +822,7 @@ public class IndexResolverFieldNamesTests extends ESTestCase {
         assertFieldNames(
             """
                 from employees
-                | eval hire_year_str = date_format(hire_date, "yyyy")
+                | eval hire_year_str = date_format("yyyy", hire_date)
                 | stats g = count_distinct(gender), h = count_distinct(hire_year_str)""",
             Set.of("hire_date", "hire_date.*", "gender", "gender.*")
         );