Skip to content

Commit

Permalink
SQL: Implement DATE_PARSE function for parsing strings into DATE valu…
Browse files Browse the repository at this point in the history
…es (#57391) (#59699)

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>
Co-authored-by: Patrick Jiang(白泽) <dreamlike.sky@foxmail.com>

(cherry picked from commit 647a413d9b21bd3938f1716bb19f8407e1334125)
  • Loading branch information
matriv authored Jul 16, 2020
1 parent 305b46c commit c7efbc1
Show file tree
Hide file tree
Showing 14 changed files with 349 additions and 23 deletions.
44 changes: 44 additions & 0 deletions docs/reference/sql/functions/date-time.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
1 change: 1 addition & 0 deletions docs/reference/sql/functions/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 102 additions & 0 deletions x-pack/plugin/sql/qa/server/src/main/resources/date.csv-spec
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 26 additions & 0 deletions x-pack/plugin/sql/qa/server/src/main/resources/docs/docs.csv-spec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2906,6 +2907,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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
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;
Expand Down Expand Up @@ -176,7 +177,8 @@ private static FunctionDefinition[][] functions() {
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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
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;
Expand All @@ -30,15 +34,16 @@
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ public static Object dateTrunc(String truncateTo, Object dateTimeOrInterval, Str
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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ public static int getNanoPrecision(Expression precisionExpression, int nano) {
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);
}
Expand Down Expand Up @@ -205,6 +209,8 @@ public static TemporalAccessor atTimeZone(TemporalAccessor ta, ZoneId zoneId) {
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ public static DateTimeParsePipe randomDateTimeParsePipe() {
randomStringLiteral(),
randomZone()
).makePipe());
functions.add(new DateParse(
randomSource(),
randomStringLiteral(),
randomStringLiteral(),
randomZone()
).makePipe());
return (DateTimeParsePipe) randomFrom(functions);
}

Expand Down
Loading

0 comments on commit c7efbc1

Please sign in to comment.