Browse Source

SQL: Implement DATE_PARSE function for parsing strings into DATE values (#57391)

Implement DATE_PARSE(<date_str>, <pattern_str>) function
which allows to parse a date string according to the specified
pattern into a date object. The patterns allowed are those of
java.time.format.DateTimeFormatter.

Closes #54962

Co-authored-by: Marios Trivyzas <matriv@users.noreply.github.com>
Patrick Jiang(白泽) 5 years ago
parent
commit
647a413d9b

+ 44 - 0
docs/reference/sql/functions/date-time.asciidoc

@@ -404,6 +404,50 @@ include-tagged::{sql-specs}/docs/docs.csv-spec[dateDiffDateTimeMinutes]
 include-tagged::{sql-specs}/docs/docs.csv-spec[dateDiffDateMinutes]
 --------------------------------------------------
 
+[[sql-functions-datetime-dateparse]]
+==== `DATE_PARSE`
+
+.Synopsis:
+[source, sql]
+--------------------------------------------------
+DATE_PARSE(
+    string_exp, <1>
+    string_exp) <2>
+--------------------------------------------------
+
+*Input*:
+
+<1> date expression as a string
+<2> parsing pattern
+
+*Output*: date
+
+*Description*: Returns a date by parsing the 1st argument using the format specified in the 2nd argument. The parsing
+format pattern used is the one from
+https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/time/format/DateTimeFormatter.html[`java.time.format.DateTimeFormatter`].
+If any of the two arguments is `null` or an empty string, then `null` is returned.
+
+[NOTE]
+If the parsing pattern does not contain all valid date units (e.g. 'HH:mm:ss', 'dd-MM HH:mm:ss', etc.) an error is returned
+as the function needs to return a value of `date` type which will contain date part.
+
+[source, sql]
+--------------------------------------------------
+include-tagged::{sql-specs}/docs/docs.csv-spec[dateParse1]
+--------------------------------------------------
+
+[NOTE]
+====
+The resulting `date` will have the time zone specified by the user through the 
+<<sql-rest-fields-timezone,`time_zone`>>/<<jdbc-cfg-timezone,`timezone`>> REST/driver parameters
+with no conversion applied.
+
+[source, sql]
+--------------------------------------------------
+include-tagged::{sql-specs}/docs/docs.csv-spec[dateParse2]
+--------------------------------------------------
+====
+
 [[sql-functions-datetime-datetimeformat]]
 ==== `DATETIME_FORMAT`
 

+ 1 - 0
docs/reference/sql/functions/index.asciidoc

@@ -55,6 +55,7 @@
 ** <<sql-functions-current-timestamp>>
 ** <<sql-functions-datetime-add>>
 ** <<sql-functions-datetime-diff>>
+** <<sql-functions-datetime-dateparse>>
 ** <<sql-functions-datetime-datetimeformat>>
 ** <<sql-functions-datetime-datetimeparse>>
 ** <<sql-functions-datetime-timeparse>>

+ 1 - 0
x-pack/plugin/sql/qa/server/src/main/resources/command.csv-spec

@@ -51,6 +51,7 @@ DATETIME_PARSE   |SCALAR
 DATETRUNC        |SCALAR
 DATE_ADD         |SCALAR
 DATE_DIFF        |SCALAR
+DATE_PARSE       |SCALAR
 DATE_PART        |SCALAR
 DATE_TRUNC       |SCALAR
 DAY              |SCALAR

+ 102 - 0
x-pack/plugin/sql/qa/server/src/main/resources/date.csv-spec

@@ -164,3 +164,105 @@ SELECT MAX(salary) FROM test_emp GROUP BY TODAY();
 ---------------
 74999
 ;
+
+selectDateParse
+schema::date1:date
+SELECT DATE_PARSE('07/04/2020', 'dd/MM/uuuu') AS date1;
+
+   date1       
+------------
+2020-04-07 
+;
+
+
+selectDateParseWithField
+schema::birth_date:ts|dp_birth_date:date
+SELECT birth_date, DATE_PARSE(DATETIME_FORMAT(birth_date, 'MM/dd/ HH uuuu'), concat(gender, 'M/dd/ HH uuuu')) AS dp_birth_date
+FROM test_emp WHERE gender = 'M' AND emp_no BETWEEN 10037 AND 10052 ORDER BY emp_no;
+
+       birth_date        | dp_birth_date
+-------------------------+----------------
+1963-07-22 00:00:00.000Z | 1963-07-22
+1960-07-20 00:00:00.000Z | 1960-07-20
+1959-10-01 00:00:00.000Z | 1959-10-01
+null                     | null
+null                     | null
+null                     | null
+null                     | null
+null                     | null
+1958-05-21 00:00:00.000Z | 1958-05-21
+1953-07-28 00:00:00.000Z | 1953-07-28
+1961-02-26 00:00:00.000Z | 1961-02-26
+;
+
+dateParseWhere
+schema::birth_date:ts|dp_birth_date:date
+SELECT birth_date, DATE_PARSE(DATETIME_FORMAT(birth_date, 'MM_dd_uuuu'), 'MM_dd_uuuu') AS dp_birth_date
+FROM test_emp WHERE dp_birth_date > '1963-10-20'::date ORDER BY emp_no;
+
+       birth_date        | dp_birth_date
+-------------------------+----------------
+1964-06-02 00:00:00.000Z | 1964-06-02
+1963-11-26 00:00:00.000Z | 1963-11-26
+1964-04-18 00:00:00.000Z | 1964-04-18
+1964-10-18 00:00:00.000Z | 1964-10-18
+1964-06-11 00:00:00.000Z | 1964-06-11
+1965-01-03 00:00:00.000Z | 1965-01-03
+;
+
+dateParseOrderBy
+schema::birth_date:ts|dp_birth_date:date
+SELECT birth_date, DATE_PARSE(DATETIME_FORMAT(birth_date, 'MM/dd/uuuu'), 'MM/dd/uuuu') AS dp_birth_date
+FROM test_emp ORDER BY 2 DESC NULLS LAST LIMIT 10;
+
+       birth_date        | dp_birth_date
+-------------------------+---------------
+1965-01-03 00:00:00.000Z | 1965-01-03
+1964-10-18 00:00:00.000Z | 1964-10-18
+1964-06-11 00:00:00.000Z | 1964-06-11
+1964-06-02 00:00:00.000Z | 1964-06-02
+1964-04-18 00:00:00.000Z | 1964-04-18
+1963-11-26 00:00:00.000Z | 1963-11-26
+1963-09-09 00:00:00.000Z | 1963-09-09
+1963-07-22 00:00:00.000Z | 1963-07-22
+1963-06-07 00:00:00.000Z | 1963-06-07
+1963-06-01 00:00:00.000Z | 1963-06-01
+;
+
+dateParseGroupBy
+schema::count:l|df_birth_date:s
+SELECT count(*) AS count, DATETIME_FORMAT(DATE_PARSE(DATETIME_FORMAT(birth_date, 'dd/MM/uuuu'), 'dd/MM/uuuu'), 'MM') AS df_birth_date
+FROM test_emp GROUP BY df_birth_date ORDER BY 1 DESC, 2 DESC NULLS LAST LIMIT 10;
+
+ count | df_birth_date
+-------+---------------
+10     | 09
+10     | 05
+10     | null
+9      | 10
+9      | 07
+8      | 11
+8      | 04
+8      | 02
+7      | 12
+7      | 06
+;
+
+dateParseHaving
+schema::max:ts|df_birth_date:s
+SELECT MAX(birth_date) AS max, DATETIME_FORMAT(birth_date, 'MM') AS df_birth_date FROM test_emp GROUP BY df_birth_date
+HAVING DATE_PARSE(DATETIME_FORMAT(MAX(birth_date), 'dd/MM/uuuu'), 'dd/MM/uuuu') > '1961-10-20'::date  ORDER BY 1 DESC NULLS LAST;
+
+          max            | df_birth_date
+-------------------------+---------------
+1965-01-03 00:00:00.000Z | 01
+1964-10-18 00:00:00.000Z | 10
+1964-06-11 00:00:00.000Z | 06
+1964-04-18 00:00:00.000Z | 04
+1963-11-26 00:00:00.000Z | 11
+1963-09-09 00:00:00.000Z | 09
+1963-07-22 00:00:00.000Z | 07
+1963-03-21 00:00:00.000Z | 03
+1962-12-29 00:00:00.000Z | 12
+null                     | null
+;

+ 26 - 0
x-pack/plugin/sql/qa/server/src/main/resources/docs/docs.csv-spec

@@ -247,6 +247,7 @@ DATETIME_PARSE   |SCALAR
 DATETRUNC        |SCALAR
 DATE_ADD         |SCALAR
 DATE_DIFF        |SCALAR
+DATE_PARSE       |SCALAR
 DATE_PART        |SCALAR
 DATE_TRUNC       |SCALAR
 DAY              |SCALAR
@@ -2907,6 +2908,31 @@ schema::time:time
 // end::timeParse3
 ;
 
+dateParse1
+schema::date:date
+// tag::dateParse1
+SELECT DATE_PARSE('07/04/2020', 'dd/MM/uuuu') AS "date";
+
+   date
+-----------
+2020-04-07
+// end::dateParse1
+;
+
+dateParse2-Ignore
+schema::date:date
+// tag::dateParse2
+{
+    "query" : "SELECT DATE_PARSE('07/04/2020', 'dd/MM/uuuu') AS \"date\"",
+    "time_zone" : "Europe/Athens"
+}
+
+   date
+------------
+2020-04-07T00:00:00.000+03:00
+// end::dateParse2
+;
+
 datePartDateTimeYears
 // tag::datePartDateTimeYears
 SELECT DATE_PART('year', '2019-09-22T11:22:33.123Z'::datetime) AS "years";

+ 3 - 1
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/SqlFunctionRegistry.java

@@ -35,6 +35,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.CurrentTi
 import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateAdd;
 import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateDiff;
 import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DatePart;
+import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateParse;
 import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFormat;
 import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeParse;
 import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTrunc;
@@ -176,7 +177,8 @@ public class SqlFunctionRegistry extends FunctionRegistry {
                 def(DayOfYear.class, DayOfYear::new, "DAY_OF_YEAR", "DAYOFYEAR", "DOY"),
                 def(DateAdd.class, DateAdd::new, "DATEADD", "DATE_ADD", "TIMESTAMPADD", "TIMESTAMP_ADD"),
                 def(DateDiff.class, DateDiff::new, "DATEDIFF", "DATE_DIFF", "TIMESTAMPDIFF", "TIMESTAMP_DIFF"),
-                def(DatePart.class, DatePart::new, "DATEPART", "DATE_PART"),
+                def(DateParse.class, DateParse::new, "DATE_PARSE"),
+                def(DatePart.class, DatePart::new, "DATEPART", "DATE_PART"), 
                 def(DateTimeFormat.class, DateTimeFormat::new, "DATETIME_FORMAT"),
                 def(DateTimeParse.class, DateTimeParse::new, "DATETIME_PARSE"),
                 def(DateTrunc.class, DateTrunc::new, "DATETRUNC", "DATE_TRUNC"),

+ 50 - 0
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateParse.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+package org.elasticsearch.xpack.sql.expression.function.scalar.datetime;
+
+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.DataType;
+import org.elasticsearch.xpack.sql.type.SqlDataTypes;
+
+import java.time.ZoneId;
+
+import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeParseProcessor.Parser.DATE;
+
+public class DateParse extends BaseDateTimeParseFunction {
+    
+    public DateParse(Source source, Expression datePart, Expression timestamp, ZoneId zoneId) {
+        super(source, datePart, timestamp, zoneId);
+    }
+
+    @Override
+    protected DateTimeParseProcessor.Parser parser() {
+        return DATE;
+    }
+
+    @Override
+    protected NodeInfo.NodeCtor3<Expression, Expression, ZoneId, BaseDateTimeParseFunction> ctorForInfo() {
+        return DateParse::new;
+    }
+
+    @Override
+    protected BinaryScalarFunction replaceChildren(Expression timestamp, Expression pattern) {
+        return new DateParse(source(), timestamp, pattern, zoneId());
+    }
+
+    @Override
+    public DataType dataType() {
+        return SqlDataTypes.DATE;
+    }
+
+    @Override
+    protected String scriptMethodName() {
+        return "dateParse";
+    }
+}

+ 9 - 4
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeParseProcessor.java

@@ -8,11 +8,15 @@ package org.elasticsearch.xpack.sql.expression.function.scalar.datetime;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.xpack.ql.expression.gen.processor.Processor;
+import org.elasticsearch.xpack.ql.type.DataType;
+import org.elasticsearch.xpack.ql.type.DataTypes;
 import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
+import org.elasticsearch.xpack.sql.type.SqlDataTypes;
 import org.elasticsearch.xpack.sql.util.DateUtils;
 
 import java.io.IOException;
 import java.time.DateTimeException;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.OffsetTime;
@@ -30,15 +34,16 @@ import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
 public class DateTimeParseProcessor extends BinaryDateTimeProcessor {
 
     public enum Parser {
-        DATE_TIME("datetime", ZonedDateTime::from, LocalDateTime::from), 
-        TIME("time", OffsetTime::from, LocalTime::from);
+        DATE_TIME(DataTypes.DATETIME, ZonedDateTime::from, LocalDateTime::from), 
+        TIME(SqlDataTypes.TIME, OffsetTime::from, LocalTime::from),
+        DATE(SqlDataTypes.DATE, LocalDate::from, (TemporalAccessor ta) -> {throw new DateTimeException("InvalidDate");});
         
         private final BiFunction<String, String, TemporalAccessor> parser;
         
         private final String parseType;
 
-        Parser(String parseType,  TemporalQuery<?>... queries) {
-            this.parseType = parseType;
+        Parser(DataType parseType, TemporalQuery<?>... queries) {
+            this.parseType = parseType.typeName();
             this.parser = (timestampStr, pattern) -> DateTimeFormatter.ofPattern(pattern, Locale.ROOT)
                     .parseBest(timestampStr, queries);
         }

+ 4 - 0
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java

@@ -280,6 +280,10 @@ public class InternalSqlScriptUtils extends InternalQlScriptUtils {
         return DateTruncProcessor.process(truncateTo, asDateTime(dateTimeOrInterval), ZoneId.of(tzId));
     }
 
+    public static Object dateParse(String dateField, String pattern, String tzId) {
+        return Parser.DATE.parse(dateField, pattern, ZoneId.of(tzId));
+    }
+
     public static Integer datePart(String dateField, Object dateTime, String tzId) {
         return (Integer) DatePartProcessor.process(dateField, asDateTime(dateTime), ZoneId.of(tzId));
     }

+ 7 - 1
x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/util/DateUtils.java

@@ -177,7 +177,11 @@ public final class DateUtils {
         nano = nano - nano % (int) Math.pow(10, (9 - precision));
         return nano;
     }
-
+    
+    public static ZonedDateTime atTimeZone(LocalDate ld, ZoneId zoneId) {
+        return ld.atStartOfDay(zoneId);
+    }
+    
     public static ZonedDateTime atTimeZone(LocalDateTime ldt, ZoneId zoneId) {
         return ZonedDateTime.ofInstant(ldt, zoneId.getRules().getValidOffsets(ldt).get(0), zoneId);
     }
@@ -205,6 +209,8 @@ public final class DateUtils {
             return atTimeZone((OffsetTime) ta, zoneId);
         } else if (ta instanceof LocalTime) {
             return atTimeZone((LocalTime) ta, zoneId);
+        } else if (ta instanceof LocalDate) {
+            return atTimeZone((LocalDate) ta, zoneId);
         } else {
             return ta;
         }

+ 1 - 0
x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt

@@ -132,6 +132,7 @@ class org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalS
   ZonedDateTime dateAdd(String, Integer, Object, String)
   Integer dateDiff(String, Object, Object, String)
   def dateTrunc(String, Object, String)
+  def dateParse(String, String, String)
   Integer datePart(String, Object, String)
   String dateTimeFormat(Object, String, String)
   def dateTimeParse(String, String, String)

+ 6 - 0
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeParsePipeTests.java

@@ -43,6 +43,12 @@ public class DateTimeParsePipeTests extends AbstractNodeTestCase<DateTimeParsePi
                 randomStringLiteral(),
                 randomZone()
         ).makePipe());
+        functions.add(new DateParse(
+                randomSource(),
+                randomStringLiteral(),
+                randomStringLiteral(),
+                randomZone()
+        ).makePipe());
         return (DateTimeParsePipe) randomFrom(functions);
     }
 

+ 90 - 17
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeParseProcessorTests.java

@@ -22,6 +22,7 @@ import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTest
 import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTestUtils.randomStringLiteral;
 import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeTestUtils.dateTime;
 import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeTestUtils.time;
+import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeTestUtils.date;
 
 public class DateTimeParseProcessorTests extends AbstractSqlWireSerializingTestCase<DateTimeParseProcessor> {
 
@@ -87,7 +88,7 @@ public class DateTimeParseProcessorTests extends AbstractSqlWireSerializingTestC
             () -> new DateTimeParse(Source.EMPTY, l("2020-04-07"), l("MM/dd"), randomZone()).makePipe().asProcessor().process(null)
         );
         assertEquals(
-            "Invalid datetime string [2020-04-07] or pattern [MM/dd] is received; Text '2020-04-07' could not be parsed at index 2",
+            "Invalid datetime string [2020-04-07] or pattern [MM/dd] is received; Text '2020-04-07' could not be parsed at index 2",        
             siae.getMessage()
         );
 
@@ -113,44 +114,98 @@ public class DateTimeParseProcessorTests extends AbstractSqlWireSerializingTestC
     
     public void testTimeInvalidInputs() {
         SqlIllegalArgumentException siae = expectThrows(
-                SqlIllegalArgumentException.class,
-                () -> new TimeParse(Source.EMPTY, l(10), randomStringLiteral(), randomZone()).makePipe().asProcessor().process(null)
+            SqlIllegalArgumentException.class,
+            () -> new TimeParse(Source.EMPTY, l(10), randomStringLiteral(), randomZone()).makePipe().asProcessor().process(null)
         );
         assertEquals("A string is required; received [10]", siae.getMessage());
         
         siae = expectThrows(
-                SqlIllegalArgumentException.class,
-                () -> new TimeParse(Source.EMPTY, randomStringLiteral(), l(20), randomZone()).makePipe().asProcessor().process(null)
+            SqlIllegalArgumentException.class,
+            () -> new TimeParse(Source.EMPTY, randomStringLiteral(), l(20), randomZone()).makePipe().asProcessor().process(null)
         );
         assertEquals("A string is required; received [20]", siae.getMessage());
         
         siae = expectThrows(
-                SqlIllegalArgumentException.class,
-                () -> new TimeParse(Source.EMPTY, l("11:04:07"), l("invalid"), randomZone()).makePipe().asProcessor().process(null)
+            SqlIllegalArgumentException.class,
+            () -> new TimeParse(Source.EMPTY, l("11:04:07"), l("invalid"), randomZone()).makePipe().asProcessor().process(null)
         );
         assertEquals(
-                "Invalid time string [11:04:07] or pattern [invalid] is received; Unknown pattern letter: i",
-                siae.getMessage()
+            "Invalid time string [11:04:07] or pattern [invalid] is received; Unknown pattern letter: i",
+            siae.getMessage()
         );
         
         siae = expectThrows(
-                SqlIllegalArgumentException.class,
-                () -> new TimeParse(Source.EMPTY, l("11:04:07"), l("HH:mm"), randomZone()).makePipe().asProcessor().process(null)
+            SqlIllegalArgumentException.class,
+            () -> new TimeParse(Source.EMPTY, l("11:04:07"), l("HH:mm"), randomZone()).makePipe().asProcessor().process(null)
         );
         assertEquals(
-                "Invalid time string [11:04:07] or pattern [HH:mm] is received; " +
-                        "Text '11:04:07' could not be parsed, unparsed text found at index 5",
-                siae.getMessage()
+            "Invalid time string [11:04:07] or pattern [HH:mm] is received; " +
+                "Text '11:04:07' could not be parsed, unparsed text found at index 5",
+            siae.getMessage()
         );
         
         siae = expectThrows(
-                SqlIllegalArgumentException.class,
-                () -> new TimeParse(Source.EMPTY, l("07/05/2020"), l("dd/MM/uuuu"), randomZone()).makePipe().asProcessor().process(null)
+            SqlIllegalArgumentException.class,
+            () -> new TimeParse(Source.EMPTY, l("07/05/2020"), l("dd/MM/uuuu"), randomZone()).makePipe().asProcessor().process(null)
         );
         assertEquals(
-                "Invalid time string [07/05/2020] or pattern [dd/MM/uuuu] is received; Unable to convert parsed text into [time]",
+            "Invalid time string [07/05/2020] or pattern [dd/MM/uuuu] is received; Unable to convert parsed text into [time]",
+            siae.getMessage()
+        );
+    }
+ 
+    public void testDateInvalidInputs() {
+        SqlIllegalArgumentException siae = expectThrows(
+            SqlIllegalArgumentException.class,
+            () -> new DateParse(Source.EMPTY, l(10), randomStringLiteral(), randomZone()).makePipe().asProcessor().process(null)
+        );
+        assertEquals("A string is required; received [10]", siae.getMessage());
+
+        siae = expectThrows(
+            SqlIllegalArgumentException.class,
+            () -> new DateParse(Source.EMPTY, randomStringLiteral(), l(20), randomZone()).makePipe().asProcessor().process(null)
+        );
+        assertEquals("A string is required; received [20]", siae.getMessage());
+
+        siae = expectThrows(
+            SqlIllegalArgumentException.class,
+            () -> new DateParse(Source.EMPTY, l("07/05/2020"), l("invalid"), randomZone()).makePipe().asProcessor().process(null)
+        );
+        assertEquals(
+            "Invalid date string [07/05/2020] or pattern [invalid] is received; Unknown pattern letter: i",
                 siae.getMessage()
         );
+
+        siae = expectThrows(
+             SqlIllegalArgumentException.class,
+             () -> new DateParse(Source.EMPTY, l("07/05/2020"), l("dd/MM"), randomZone()).makePipe().asProcessor().process(null)
+        );
+        assertEquals(
+           "Invalid date string [07/05/2020] or pattern [dd/MM] is received; " +
+                "Text '07/05/2020' could not be parsed, unparsed text found at index 5",
+           siae.getMessage()
+        );
+    
+        siae = expectThrows(
+            SqlIllegalArgumentException.class,
+            () -> new DateParse(Source.EMPTY, l("11:04:07"), l("HH:mm:ss"), randomZone()).makePipe().asProcessor().process(null)
+        );
+        assertEquals(
+            "Invalid date string [11:04:07] or pattern [HH:mm:ss] is received; Unable to convert parsed text into [date]",
+            siae.getMessage()
+        );
+        
+        siae = expectThrows(
+            SqlIllegalArgumentException.class,
+            () -> new DateParse(Source.EMPTY, l("05/2020 11:04:07"), l("MM/uuuu HH:mm:ss"), randomZone())
+                .makePipe()
+                .asProcessor()
+                .process(null)
+        );
+        assertEquals(
+            "Invalid date string [05/2020 11:04:07] or pattern [MM/uuuu HH:mm:ss] is received; Unable to convert parsed text into [date]",
+            siae.getMessage()
+        );
     }
 
     public void testWithNulls() {
@@ -164,6 +219,11 @@ public class DateTimeParseProcessorTests extends AbstractSqlWireSerializingTestC
         assertNull(new TimeParse(Source.EMPTY, randomStringLiteral(), l(""), randomZone()).makePipe().asProcessor().process(null));
         assertNull(new TimeParse(Source.EMPTY, NULL, randomStringLiteral(), randomZone()).makePipe().asProcessor().process(null));
         assertNull(new TimeParse(Source.EMPTY, l(""), randomStringLiteral(), randomZone()).makePipe().asProcessor().process(null));
+        // DateParse
+        assertNull(new DateParse(Source.EMPTY, randomStringLiteral(), NULL, randomZone()).makePipe().asProcessor().process(null));
+        assertNull(new DateParse(Source.EMPTY, randomStringLiteral(), l(""), randomZone()).makePipe().asProcessor().process(null));
+        assertNull(new DateParse(Source.EMPTY, NULL, randomStringLiteral(), randomZone()).makePipe().asProcessor().process(null));
+        assertNull(new DateParse(Source.EMPTY, l(""), randomStringLiteral(), randomZone()).makePipe().asProcessor().process(null));
     }
 
     public void testParsing() {
@@ -203,6 +263,19 @@ public class DateTimeParseProcessorTests extends AbstractSqlWireSerializingTestC
                 .asProcessor()
                 .process(null)
         );
+        // DateParse
+        assertEquals(
+            date(2020, 4, 7, zoneId),
+            new DateParse(Source.EMPTY, l("07/04/2020"), l("dd/MM/uuuu"), zoneId).makePipe()
+                .asProcessor()
+                .process(null)
+        );
+        assertEquals(
+                date(2020, 4, 7, zoneId),
+                new DateParse(Source.EMPTY, l("07/04/2020 12:12:00"), l("dd/MM/uuuu HH:mm:ss"), zoneId).makePipe()
+                        .asProcessor()
+                        .process(null)
+        );
         assertEquals(
             time(10, 20, 30, 123456789, ZoneOffset.of("+05:30"), zoneId),
             new TimeParse(Source.EMPTY, l("16/06/2020 10:20:30.123456789 +05:30"), l("dd/MM/uuuu HH:mm:ss.SSSSSSSSS zz"), zoneId).makePipe()

+ 4 - 0
x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeTestUtils.java

@@ -63,6 +63,10 @@ public class DateTimeTestUtils {
         return OffsetTime.of(lt, zoneId.getRules().getValidOffsets(ldt).get(0));
     }
 
+    public static ZonedDateTime date(int year, int month, int day, ZoneId zoneId) {
+        return LocalDate.of(year, month, day).atStartOfDay(zoneId);
+    }
+
     static ZonedDateTime nowWithMillisResolution() {
         Clock millisResolutionClock = Clock.tick(Clock.systemUTC(), Duration.ofMillis(1));
         return ZonedDateTime.now(millisResolutionClock);