Implement FORMAT according to the SQL Server spec: https://docs.microsoft.com/en-us/sql/t-sql/functions/format-transact-sql?view=sql-server-ver15#ExampleD by translating to the java.time patterns used in DATETIME_FORMAT. Closes: #54965 Co-authored-by: Marios Trivyzas <matriv@users.noreply.github.com> Co-authored-by: Bogdan Pintea <bogdan.pintea@elastic.co> Co-authored-by: Andrei Stefan <astefan@users.noreply.github.com> (cherry picked from commit da511f4e033db6e8a6aa2a54b23e906b5e026845)
This commit is contained in:
parent
cadd5dc53f
commit
1f612cccbb
|
@ -579,9 +579,9 @@ include-tagged::{sql-specs}/docs/docs.csv-spec[timeParse2]
|
|||
|
||||
[NOTE]
|
||||
====
|
||||
If timezone is not specified in the time string expression and the parsing pattern,
|
||||
If timezone is not specified in the time string expression and the parsing pattern,
|
||||
the resulting `time` will have the offset of the time zone specified by the user through the
|
||||
<<sql-rest-fields-timezone,`time_zone`>>/<<jdbc-cfg-timezone,`timezone`>> REST/driver
|
||||
<<sql-rest-fields-timezone,`time_zone`>>/<<jdbc-cfg-timezone,`timezone`>> REST/driver
|
||||
parameters at the Unix epoch date (`1970-01-01`) with no conversion applied.
|
||||
|
||||
[source, sql]
|
||||
|
@ -765,6 +765,59 @@ include-tagged::{sql-specs}/docs/docs.csv-spec[truncateIntervalHour]
|
|||
include-tagged::{sql-specs}/docs/docs.csv-spec[truncateIntervalDay]
|
||||
--------------------------------------------------
|
||||
|
||||
[[sql-functions-datetime-format]]
|
||||
==== `FORMAT`
|
||||
|
||||
.Synopsis:
|
||||
[source, sql]
|
||||
--------------------------------------------------
|
||||
FORMAT(
|
||||
date_exp/datetime_exp/time_exp, <1>
|
||||
string_exp) <2>
|
||||
--------------------------------------------------
|
||||
|
||||
*Input*:
|
||||
|
||||
<1> date/datetime/time expression
|
||||
<2> format pattern
|
||||
|
||||
*Output*: string
|
||||
|
||||
*Description*: Returns the date/datetime/time as a string using the
|
||||
https://docs.microsoft.com/en-us/sql/t-sql/functions/format-transact-sql#arguments[format] specified in the 2nd argument. The formatting
|
||||
pattern used is the one from
|
||||
https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings[Microsoft SQL Server Format Specification].
|
||||
If any of the two arguments is `null` or the pattern is an empty string `null` is returned.
|
||||
|
||||
[NOTE]
|
||||
If the 1st argument is of type `time`, then pattern specified by the 2nd argument cannot contain date related units
|
||||
(e.g. 'dd', 'MM', 'YYYY', etc.). If it contains such units an error is returned.
|
||||
|
||||
*Special Cases*
|
||||
|
||||
- Format specifier `F` will be working similar to format specifier `f`.
|
||||
It will return the fractional part of seconds, and the number of digits will be same as of the number of `Fs` provided as input (up to 9 digits).
|
||||
Result will contain `0` appended in the end to match with number of `F` provided.
|
||||
e.g.: for a time part `10:20:30.1234` and pattern `HH:mm:ss.FFFFFF`, the output string of the function would be: `10:20:30.123400`.
|
||||
- Format Specifier `y` will return year-of-era instead of one/two low-order digits.
|
||||
eg.: For year `2009`, `y` will be returning `2009` instead of `9`. For year `43`, `y` format specifier will return `43`.
|
||||
- Special characters like `"` , `\` and `%` will be returned as it is without any change. eg.: formatting date `17-sep-2020` with `%M` will return `%9`
|
||||
|
||||
[source, sql]
|
||||
--------------------------------------------------
|
||||
include-tagged::{sql-specs}/docs/docs.csv-spec[formatDate]
|
||||
--------------------------------------------------
|
||||
|
||||
[source, sql]
|
||||
--------------------------------------------------
|
||||
include-tagged::{sql-specs}/docs/docs.csv-spec[formatDateTime]
|
||||
--------------------------------------------------
|
||||
|
||||
[source, sql]
|
||||
--------------------------------------------------
|
||||
include-tagged::{sql-specs}/docs/docs.csv-spec[formatTime]
|
||||
--------------------------------------------------
|
||||
|
||||
[[sql-functions-datetime-day]]
|
||||
==== `DAY_OF_MONTH/DOM/DAY`
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
** <<sql-functions-datetime-dateparse>>
|
||||
** <<sql-functions-datetime-datetimeformat>>
|
||||
** <<sql-functions-datetime-datetimeparse>>
|
||||
** <<sql-functions-datetime-format>>
|
||||
** <<sql-functions-datetime-timeparse>>
|
||||
** <<sql-functions-datetime-part>>
|
||||
** <<sql-functions-datetime-trunc>>
|
||||
|
|
|
@ -66,6 +66,7 @@ DAY_OF_YEAR |SCALAR
|
|||
DOM |SCALAR
|
||||
DOW |SCALAR
|
||||
DOY |SCALAR
|
||||
FORMAT |SCALAR
|
||||
HOUR |SCALAR
|
||||
HOUR_OF_DAY |SCALAR
|
||||
IDOW |SCALAR
|
||||
|
|
|
@ -1017,6 +1017,120 @@ F | 1997-05-19 00:00:00.000Z
|
|||
M | 1996-11-05 00:00:00.000Z
|
||||
;
|
||||
|
||||
selectFormat
|
||||
schema::format_date:s|format_datetime:s|format_time:s
|
||||
SELECT FORMAT('2020-04-05T11:22:33.123Z'::date, 'dd/MM/YYYY HH:mm:ss.fff') AS format_date,
|
||||
FORMAT('2020-04-05T11:22:33.123Z'::datetime, 'dd/MM/YYYY HH:mm:ss.ff') AS format_datetime,
|
||||
FORMAT('11:22:33.123456789Z'::time, 'HH:mm:ss.ff') AS format_time;
|
||||
|
||||
format_date | format_datetime | format_time
|
||||
------------------------+------------------------+----------------
|
||||
05/04/2020 00:00:00.000 | 05/04/2020 11:22:33.12 | 11:22:33.12
|
||||
;
|
||||
|
||||
selectFormatWithLength
|
||||
schema::format_datetime:s|length:i
|
||||
SELECT FORMAT('2020-04-05T11:22:33.123Z'::datetime, 'dd/MM/YYYY HH:mm:ss.ff') AS format_datetime,
|
||||
LENGTH(FORMAT('2020-04-05T11:22:33.123Z'::datetime, 'dd/MM/YYYY HH:mm:ss.ff')) AS length;
|
||||
|
||||
format_datetime | length
|
||||
------------------------+----------------
|
||||
05/04/2020 11:22:33.12 | 22
|
||||
;
|
||||
|
||||
selectFormatWithField
|
||||
schema::birth_date:ts|format_birth_date1:s|format_birth_date2:s|emp_no:i
|
||||
SELECT birth_date, FORMAT(birth_date, 'MM/dd/YYYY') AS format_birth_date1, FORMAT(birth_date, concat(gender, 'M/dd')) AS format_birth_date2, emp_no
|
||||
FROM test_emp WHERE gender = 'M' AND emp_no BETWEEN 10037 AND 10052 ORDER BY emp_no;
|
||||
|
||||
birth_date | format_birth_date1 | format_birth_date2 | emp_no
|
||||
-------------------------+--------------------+--------------------+----------
|
||||
1963-07-22 00:00:00.000Z | 07/22/1963 | 07/22 | 10037
|
||||
1960-07-20 00:00:00.000Z | 07/20/1960 | 07/20 | 10038
|
||||
1959-10-01 00:00:00.000Z | 10/01/1959 | 10/01 | 10039
|
||||
null | null | null | 10043
|
||||
null | null | null | 10045
|
||||
null | null | null | 10046
|
||||
null | null | null | 10047
|
||||
null | null | null | 10048
|
||||
1958-05-21 00:00:00.000Z | 05/21/1958 | 05/21 | 10050
|
||||
1953-07-28 00:00:00.000Z | 07/28/1953 | 07/28 | 10051
|
||||
1961-02-26 00:00:00.000Z | 02/26/1961 | 02/26 | 10052
|
||||
;
|
||||
|
||||
formatWhere
|
||||
schema::birth_date:ts|format_birth_date:s|emp_no:i
|
||||
SELECT birth_date, FORMAT(birth_date, 'MM') AS format_birth_date, emp_no FROM test_emp
|
||||
WHERE FORMAT(birth_date, 'MM')::integer > 10 ORDER BY emp_no LIMIT 10;
|
||||
|
||||
birth_date | format_birth_date | emp_no
|
||||
-------------------------+-------------------+----------
|
||||
1959-12-03 00:00:00.000Z | 12 | 10003
|
||||
1953-11-07 00:00:00.000Z | 11 | 10011
|
||||
1952-12-24 00:00:00.000Z | 12 | 10020
|
||||
1963-11-26 00:00:00.000Z | 11 | 10028
|
||||
1956-12-13 00:00:00.000Z | 12 | 10029
|
||||
1956-11-14 00:00:00.000Z | 11 | 10033
|
||||
1962-12-29 00:00:00.000Z | 12 | 10034
|
||||
1961-11-02 00:00:00.000Z | 11 | 10062
|
||||
1952-11-13 00:00:00.000Z | 11 | 10066
|
||||
1962-11-26 00:00:00.000Z | 11 | 10068
|
||||
;
|
||||
|
||||
formatOrderBy
|
||||
schema::birth_date:ts|format_birth_date:s
|
||||
SELECT birth_date, FORMAT(birth_date, 'MM/dd/YYYY') AS format_birth_date FROM test_emp ORDER BY 2 DESC NULLS LAST LIMIT 10;
|
||||
|
||||
birth_date | format_birth_date
|
||||
-------------------------+---------------
|
||||
1962-12-29 00:00:00.000Z | 12/29/1962
|
||||
1959-12-25 00:00:00.000Z | 12/25/1959
|
||||
1952-12-24 00:00:00.000Z | 12/24/1952
|
||||
1960-12-17 00:00:00.000Z | 12/17/1960
|
||||
1956-12-13 00:00:00.000Z | 12/13/1956
|
||||
1959-12-03 00:00:00.000Z | 12/03/1959
|
||||
1957-12-03 00:00:00.000Z | 12/03/1957
|
||||
1963-11-26 00:00:00.000Z | 11/26/1963
|
||||
1962-11-26 00:00:00.000Z | 11/26/1962
|
||||
1962-11-19 00:00:00.000Z | 11/19/1962
|
||||
;
|
||||
|
||||
formatGroupBy
|
||||
schema::count:l|format_birth_date:s
|
||||
SELECT count(*) AS count, FORMAT(birth_date, 'MM') AS format_birth_date FROM test_emp GROUP BY format_birth_date ORDER BY 1 DESC, 2 DESC;
|
||||
|
||||
count | format_birth_date
|
||||
-------+---------------
|
||||
10 | 09
|
||||
10 | 05
|
||||
10 | null
|
||||
9 | 10
|
||||
9 | 07
|
||||
8 | 11
|
||||
8 | 04
|
||||
8 | 02
|
||||
7 | 12
|
||||
7 | 06
|
||||
6 | 08
|
||||
6 | 01
|
||||
2 | 03
|
||||
;
|
||||
|
||||
formatHaving
|
||||
schema::max:ts|format_birth_date:s
|
||||
SELECT MAX(birth_date) AS max, FORMAT(birth_date, 'MM') AS format_birth_date FROM test_emp GROUP BY format_birth_date
|
||||
HAVING FORMAT(MAX(birth_date), 'dd')::integer > 20 ORDER BY 1 DESC;
|
||||
|
||||
max | format_birth_date
|
||||
-------------------------+---------------
|
||||
1963-11-26 00:00:00.000Z | 11
|
||||
1963-07-22 00:00:00.000Z | 07
|
||||
1963-03-21 00:00:00.000Z | 03
|
||||
1962-12-29 00:00:00.000Z | 12
|
||||
1961-05-30 00:00:00.000Z | 05
|
||||
1961-02-26 00:00:00.000Z | 02
|
||||
;
|
||||
|
||||
//
|
||||
// Aggregate
|
||||
//
|
||||
|
|
|
@ -262,6 +262,7 @@ DAY_OF_YEAR |SCALAR
|
|||
DOM |SCALAR
|
||||
DOW |SCALAR
|
||||
DOY |SCALAR
|
||||
FORMAT |SCALAR
|
||||
HOUR |SCALAR
|
||||
HOUR_OF_DAY |SCALAR
|
||||
IDOW |SCALAR
|
||||
|
@ -3088,6 +3089,36 @@ SELECT DATE_TRUNC('days', INTERVAL '19 15:24:19' DAY TO SECONDS) AS day;
|
|||
// end::truncateIntervalDay
|
||||
;
|
||||
|
||||
formatDate
|
||||
// tag::formatDate
|
||||
SELECT FORMAT(CAST('2020-04-05' AS DATE), 'dd/MM/YYYY') AS "date";
|
||||
|
||||
date
|
||||
------------------
|
||||
05/04/2020
|
||||
// end::formatDate
|
||||
;
|
||||
|
||||
formatDateTime
|
||||
// tag::formatDateTime
|
||||
SELECT FORMAT(CAST('2020-04-05T11:22:33.987654' AS DATETIME), 'dd/MM/YYYY HH:mm:ss.ff') AS "datetime";
|
||||
|
||||
datetime
|
||||
------------------
|
||||
05/04/2020 11:22:33.98
|
||||
// end::formatDateTime
|
||||
;
|
||||
|
||||
formatTime
|
||||
// tag::formatTime
|
||||
SELECT FORMAT(CAST('11:22:33.987' AS TIME), 'HH mm ss.f') AS "time";
|
||||
|
||||
time
|
||||
------------------
|
||||
11 22 33.9
|
||||
// end::formatTime
|
||||
;
|
||||
|
||||
constantDayOfWeek
|
||||
// tag::dayOfWeek
|
||||
SELECT DAY_OF_WEEK(CAST('2018-02-19T10:23:27Z' AS TIMESTAMP)) AS day;
|
||||
|
|
|
@ -43,6 +43,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayName;
|
|||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayOfMonth;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayOfWeek;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayOfYear;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.Format;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.HourOfDay;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.IsoDayOfWeek;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.IsoWeekOfYear;
|
||||
|
@ -182,6 +183,7 @@ public class SqlFunctionRegistry extends FunctionRegistry {
|
|||
def(DateTimeFormat.class, DateTimeFormat::new, "DATETIME_FORMAT"),
|
||||
def(DateTimeParse.class, DateTimeParse::new, "DATETIME_PARSE"),
|
||||
def(DateTrunc.class, DateTrunc::new, "DATETRUNC", "DATE_TRUNC"),
|
||||
def(Format.class, Format::new, "FORMAT"),
|
||||
def(HourOfDay.class, HourOfDay::new, "HOUR_OF_DAY", "HOUR"),
|
||||
def(IsoDayOfWeek.class, IsoDayOfWeek::new, "ISO_DAY_OF_WEEK", "ISODAYOFWEEK", "ISODOW", "IDOW"),
|
||||
def(IsoWeekOfYear.class, IsoWeekOfYear::new, "ISO_WEEK_OF_YEAR", "ISOWEEKOFYEAR", "ISOWEEK", "IWOY", "IW"),
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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.Expressions;
|
||||
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
|
||||
import org.elasticsearch.xpack.ql.tree.NodeInfo;
|
||||
import org.elasticsearch.xpack.ql.tree.NodeInfo.NodeCtor3;
|
||||
import org.elasticsearch.xpack.ql.tree.Source;
|
||||
import org.elasticsearch.xpack.ql.type.DataType;
|
||||
import org.elasticsearch.xpack.ql.type.DataTypes;
|
||||
|
||||
import java.time.ZoneId;
|
||||
|
||||
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
|
||||
import static org.elasticsearch.xpack.sql.expression.SqlTypeResolutions.isDateOrTime;
|
||||
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFormatProcessor.Formatter;
|
||||
|
||||
public abstract class BaseDateTimeFormatFunction extends BinaryDateTimeFunction {
|
||||
public BaseDateTimeFormatFunction(Source source, Expression timestamp, Expression pattern, ZoneId zoneId) {
|
||||
super(source, timestamp, pattern, zoneId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataType dataType() {
|
||||
return DataTypes.KEYWORD;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TypeResolution resolveType() {
|
||||
TypeResolution resolution = isDateOrTime(left(), sourceText(), Expressions.ParamOrdinal.FIRST);
|
||||
if (resolution.unresolved()) {
|
||||
return resolution;
|
||||
}
|
||||
resolution = isString(right(), sourceText(), Expressions.ParamOrdinal.SECOND);
|
||||
if (resolution.unresolved()) {
|
||||
return resolution;
|
||||
}
|
||||
return TypeResolution.TYPE_RESOLVED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NodeInfo<? extends Expression> info() {
|
||||
return NodeInfo.create(this, ctor(), left(), right(), zoneId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fold() {
|
||||
return formatter().format(left().fold(), right().fold(), zoneId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pipe createPipe(Pipe timestamp, Pipe pattern, ZoneId zoneId) {
|
||||
return new DateTimeFormatPipe(source(), this, timestamp, pattern, zoneId, formatter());
|
||||
}
|
||||
|
||||
protected abstract Formatter formatter();
|
||||
|
||||
protected abstract NodeCtor3<Expression, Expression, ZoneId, BaseDateTimeFormatFunction> ctor();
|
||||
}
|
|
@ -6,51 +6,29 @@
|
|||
package org.elasticsearch.xpack.sql.expression.function.scalar.datetime;
|
||||
|
||||
import org.elasticsearch.xpack.ql.expression.Expression;
|
||||
import org.elasticsearch.xpack.ql.expression.Expressions;
|
||||
import org.elasticsearch.xpack.ql.expression.function.scalar.BinaryScalarFunction;
|
||||
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
|
||||
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.ql.type.DataTypes;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFormatProcessor.Formatter;
|
||||
|
||||
import java.time.ZoneId;
|
||||
|
||||
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
|
||||
import static org.elasticsearch.xpack.sql.expression.SqlTypeResolutions.isDateOrTime;
|
||||
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFormatProcessor.Formatter.DATE_TIME_FORMAT;
|
||||
|
||||
public class DateTimeFormat extends BinaryDateTimeFunction {
|
||||
public class DateTimeFormat extends BaseDateTimeFormatFunction {
|
||||
|
||||
public DateTimeFormat(Source source, Expression timestamp, Expression pattern, ZoneId zoneId) {
|
||||
super(source, timestamp, pattern, zoneId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataType dataType() {
|
||||
return DataTypes.KEYWORD;
|
||||
protected Formatter formatter() {
|
||||
return DATE_TIME_FORMAT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TypeResolution resolveType() {
|
||||
TypeResolution resolution = isDateOrTime(left(), sourceText(), Expressions.ParamOrdinal.FIRST);
|
||||
if (resolution.unresolved()) {
|
||||
return resolution;
|
||||
}
|
||||
resolution = isString(right(), sourceText(), Expressions.ParamOrdinal.SECOND);
|
||||
if (resolution.unresolved()) {
|
||||
return resolution;
|
||||
}
|
||||
return TypeResolution.TYPE_RESOLVED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BinaryScalarFunction replaceChildren(Expression timestamp, Expression pattern) {
|
||||
return new DateTimeFormat(source(), timestamp, pattern, zoneId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NodeInfo<? extends Expression> info() {
|
||||
return NodeInfo.create(this, DateTimeFormat::new, left(), right(), zoneId());
|
||||
protected NodeInfo.NodeCtor3<Expression, Expression, ZoneId, BaseDateTimeFormatFunction> ctor() {
|
||||
return DateTimeFormat::new;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -59,12 +37,7 @@ public class DateTimeFormat extends BinaryDateTimeFunction {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Object fold() {
|
||||
return DateTimeFormatProcessor.process(left().fold(), right().fold(), zoneId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pipe createPipe(Pipe timestamp, Pipe pattern, ZoneId zoneId) {
|
||||
return new DateTimeFormatPipe(source(), this, timestamp, pattern, zoneId);
|
||||
protected BinaryScalarFunction replaceChildren(Expression timestamp, Expression pattern) {
|
||||
return new DateTimeFormat(source(), timestamp, pattern, zoneId());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,25 +12,55 @@ import org.elasticsearch.xpack.ql.tree.NodeInfo;
|
|||
import org.elasticsearch.xpack.ql.tree.Source;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFormatProcessor.Formatter;
|
||||
|
||||
public class DateTimeFormatPipe extends BinaryDateTimePipe {
|
||||
|
||||
public DateTimeFormatPipe(Source source, Expression expression, Pipe left, Pipe right, ZoneId zoneId) {
|
||||
private final Formatter formatter;
|
||||
|
||||
public DateTimeFormatPipe(Source source, Expression expression, Pipe left, Pipe right, ZoneId zoneId, Formatter formatter) {
|
||||
super(source, expression, left, right, zoneId);
|
||||
this.formatter = formatter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NodeInfo<DateTimeFormatPipe> info() {
|
||||
return NodeInfo.create(this, DateTimeFormatPipe::new, expression(), left(), right(), zoneId());
|
||||
return NodeInfo.create(this, DateTimeFormatPipe::new, expression(), left(), right(), zoneId(), formatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DateTimeFormatPipe replaceChildren(Pipe left, Pipe right) {
|
||||
return new DateTimeFormatPipe(source(), expression(), left, right, zoneId());
|
||||
return new DateTimeFormatPipe(source(), expression(), left, right, zoneId(), formatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Processor makeProcessor(Processor left, Processor right, ZoneId zoneId) {
|
||||
return new DateTimeFormatProcessor(left, right, zoneId);
|
||||
return new DateTimeFormatProcessor(left, right, zoneId, formatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), this.formatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
if (!super.equals(o)) {
|
||||
return false;
|
||||
}
|
||||
DateTimeFormatPipe other = (DateTimeFormatPipe) o;
|
||||
return super.equals(o) && this.formatter == other.formatter;
|
||||
}
|
||||
|
||||
public Formatter formatter() {
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
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.sql.SqlIllegalArgumentException;
|
||||
|
||||
|
@ -17,55 +18,90 @@ import java.time.ZonedDateTime;
|
|||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.elasticsearch.xpack.sql.util.DateUtils.asTimeAtZone;
|
||||
|
||||
public class DateTimeFormatProcessor extends BinaryDateTimeProcessor {
|
||||
|
||||
public static final String NAME = "dtformat";
|
||||
private static final String[][] JAVA_TIME_FORMAT_REPLACEMENTS = {
|
||||
{"tt", "a"},
|
||||
{"t", "a"},
|
||||
{"dddd", "eeee"},
|
||||
{"ddd", "eee"},
|
||||
{"K", "V"},
|
||||
{"g", "G"},
|
||||
{"f", "S"},
|
||||
{"F", "S"},
|
||||
{"z", "X"}
|
||||
};
|
||||
private final Formatter formatter;
|
||||
|
||||
public DateTimeFormatProcessor(Processor source1, Processor source2, ZoneId zoneId) {
|
||||
|
||||
public enum Formatter {
|
||||
FORMAT,
|
||||
DATE_TIME_FORMAT;
|
||||
|
||||
private String getJavaPattern(String pattern) {
|
||||
if (this == FORMAT) {
|
||||
for (String[] replacement : JAVA_TIME_FORMAT_REPLACEMENTS) {
|
||||
pattern = pattern.replace(replacement[0], replacement[1]);
|
||||
}
|
||||
}
|
||||
return pattern;
|
||||
}
|
||||
|
||||
public Object format(Object timestamp, Object pattern, ZoneId zoneId) {
|
||||
if (timestamp == null || pattern == null) {
|
||||
return null;
|
||||
}
|
||||
String patternString;
|
||||
if (pattern instanceof String) {
|
||||
patternString = (String) pattern;
|
||||
} else {
|
||||
throw new SqlIllegalArgumentException("A string is required; received [{}]", pattern);
|
||||
}
|
||||
if (patternString.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (timestamp instanceof ZonedDateTime == false && timestamp instanceof OffsetTime == false) {
|
||||
throw new SqlIllegalArgumentException("A date/datetime/time is required; received [{}]", timestamp);
|
||||
}
|
||||
|
||||
TemporalAccessor ta;
|
||||
if (timestamp instanceof ZonedDateTime) {
|
||||
ta = ((ZonedDateTime) timestamp).withZoneSameInstant(zoneId);
|
||||
} else {
|
||||
ta = asTimeAtZone((OffsetTime) timestamp, zoneId);
|
||||
}
|
||||
try {
|
||||
return DateTimeFormatter.ofPattern(getJavaPattern(patternString), Locale.ROOT).format(ta);
|
||||
} catch (IllegalArgumentException | DateTimeException e) {
|
||||
throw new SqlIllegalArgumentException(
|
||||
"Invalid pattern [{}] is received for formatting date/time [{}]; {}",
|
||||
pattern,
|
||||
timestamp,
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeFormatProcessor(Processor source1, Processor source2, ZoneId zoneId, Formatter formatter) {
|
||||
super(source1, source2, zoneId);
|
||||
this.formatter = formatter;
|
||||
}
|
||||
|
||||
public DateTimeFormatProcessor(StreamInput in) throws IOException {
|
||||
super(in);
|
||||
this.formatter = in.readEnum(Formatter.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in Painless scripting
|
||||
*/
|
||||
public static Object process(Object timestamp, Object pattern, ZoneId zoneId) {
|
||||
if (timestamp == null || pattern == null) {
|
||||
return null;
|
||||
}
|
||||
if (pattern instanceof String == false) {
|
||||
throw new SqlIllegalArgumentException("A string is required; received [{}]", pattern);
|
||||
}
|
||||
if (((String) pattern).isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (timestamp instanceof ZonedDateTime == false && timestamp instanceof OffsetTime == false) {
|
||||
throw new SqlIllegalArgumentException("A date/datetime/time is required; received [{}]", timestamp);
|
||||
}
|
||||
|
||||
TemporalAccessor ta;
|
||||
if (timestamp instanceof ZonedDateTime) {
|
||||
ta = ((ZonedDateTime) timestamp).withZoneSameInstant(zoneId);
|
||||
} else {
|
||||
ta = asTimeAtZone((OffsetTime) timestamp, zoneId);
|
||||
}
|
||||
try {
|
||||
return DateTimeFormatter.ofPattern((String) pattern, Locale.ROOT).format(ta);
|
||||
} catch (IllegalArgumentException | DateTimeException e) {
|
||||
throw new SqlIllegalArgumentException(
|
||||
"Invalid pattern [{}] is received for formatting date/time [{}]; {}",
|
||||
pattern,
|
||||
timestamp,
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
@Override
|
||||
protected void doWrite(StreamOutput out) throws IOException {
|
||||
out.writeEnum(formatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -75,6 +111,29 @@ public class DateTimeFormatProcessor extends BinaryDateTimeProcessor {
|
|||
|
||||
@Override
|
||||
protected Object doProcess(Object timestamp, Object pattern) {
|
||||
return process(timestamp, pattern, zoneId());
|
||||
return this.formatter.format(timestamp, pattern, zoneId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.hashCode(), formatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DateTimeFormatProcessor other = (DateTimeFormatProcessor) obj;
|
||||
return super.equals(other) && Objects.equals(formatter, other.formatter);
|
||||
}
|
||||
|
||||
public Formatter formatter() {
|
||||
return formatter;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.sql.expression.function.scalar.datetime.DateTimeFormatProcessor.Formatter;
|
||||
|
||||
import java.time.ZoneId;
|
||||
|
||||
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFormatProcessor.Formatter.FORMAT;
|
||||
|
||||
public class Format extends BaseDateTimeFormatFunction {
|
||||
public Format(Source source, Expression timestamp, Expression pattern, ZoneId zoneId) {
|
||||
super(source, timestamp, pattern, zoneId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Formatter formatter() {
|
||||
return FORMAT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NodeInfo.NodeCtor3<Expression, Expression, ZoneId, BaseDateTimeFormatFunction> ctor() {
|
||||
return Format::new;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String scriptMethodName() {
|
||||
return "format";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BinaryScalarFunction replaceChildren(Expression timestamp, Expression pattern) {
|
||||
return new Format(source(), timestamp, pattern, zoneId());
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
|
|||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateAddProcessor;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateDiffProcessor;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DatePartProcessor;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFormatProcessor;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFormatProcessor.Formatter;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFunction;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTruncProcessor;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.NamedDateTimeProcessor.NameExtractor;
|
||||
|
@ -289,17 +289,21 @@ public class InternalSqlScriptUtils extends InternalQlScriptUtils {
|
|||
}
|
||||
|
||||
public static String dateTimeFormat(Object dateTime, String pattern, String tzId) {
|
||||
return (String) DateTimeFormatProcessor.process(asDateTime(dateTime), pattern, ZoneId.of(tzId));
|
||||
return (String) Formatter.DATE_TIME_FORMAT.format(asDateTime(dateTime), pattern, ZoneId.of(tzId));
|
||||
}
|
||||
|
||||
public static Object dateTimeParse(String dateField, String pattern, String tzId) {
|
||||
return Parser.DATE_TIME.parse(dateField, pattern, ZoneId.of(tzId));
|
||||
}
|
||||
|
||||
public static String format(Object dateTime, String pattern, String tzId) {
|
||||
return (String) Formatter.FORMAT.format(asDateTime(dateTime), pattern, ZoneId.of(tzId));
|
||||
}
|
||||
|
||||
public static Object timeParse(String dateField, String pattern, String tzId) {
|
||||
return Parser.TIME.parse(dateField, pattern, ZoneId.of(tzId));
|
||||
}
|
||||
|
||||
|
||||
public static ZonedDateTime asDateTime(Object dateTime) {
|
||||
return (ZonedDateTime) asDateTime(dateTime, false);
|
||||
}
|
||||
|
|
|
@ -135,6 +135,7 @@ class org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalS
|
|||
def dateParse(String, String, String)
|
||||
Integer datePart(String, Object, String)
|
||||
String dateTimeFormat(Object, String, String)
|
||||
String format(Object, String, String)
|
||||
def dateTimeParse(String, String, String)
|
||||
def timeParse(String, String, String)
|
||||
IntervalDayTime intervalDayTime(String, String)
|
||||
|
|
|
@ -353,6 +353,23 @@ public class VerifierErrorMessagesTests extends ESTestCase {
|
|||
);
|
||||
}
|
||||
|
||||
public void testFormatValidArgs() {
|
||||
accept("SELECT FORMAT(date, 'HH:mm:ss.fff KK') FROM test");
|
||||
accept("SELECT FORMAT(date::date, 'MM/dd/YYYY') FROM test");
|
||||
accept("SELECT FORMAT(date::time, 'HH:mm:ss Z') FROM test");
|
||||
}
|
||||
|
||||
public void testFormatInvalidArgs() {
|
||||
assertEquals(
|
||||
"1:8: first argument of [FORMAT(int, keyword)] must be [date, time or datetime], found value [int] type [integer]",
|
||||
error("SELECT FORMAT(int, keyword) FROM test")
|
||||
);
|
||||
assertEquals(
|
||||
"1:8: second argument of [FORMAT(date, int)] must be [string], found value [int] type [integer]",
|
||||
error("SELECT FORMAT(date, int) FROM test")
|
||||
);
|
||||
}
|
||||
|
||||
public void testValidDateTimeFunctionsOnTime() {
|
||||
accept("SELECT HOUR_OF_DAY(CAST(date AS TIME)) FROM test");
|
||||
accept("SELECT MINUTE_OF_HOUR(CAST(date AS TIME)) FROM test");
|
||||
|
|
|
@ -25,12 +25,17 @@ import static org.elasticsearch.xpack.ql.expression.Expressions.pipe;
|
|||
import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTestUtils.randomDatetimeLiteral;
|
||||
import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTestUtils.randomStringLiteral;
|
||||
import static org.elasticsearch.xpack.ql.tree.SourceTests.randomSource;
|
||||
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFormatProcessor.Formatter;
|
||||
|
||||
public class DateTimeFormatPipeTests extends AbstractNodeTestCase<DateTimeFormatPipe, Pipe> {
|
||||
|
||||
public static DateTimeFormatPipe randomDateTimeFormatPipe() {
|
||||
return (DateTimeFormatPipe) new DateTimeFormat(randomSource(), randomDatetimeLiteral(), randomStringLiteral(), randomZone())
|
||||
.makePipe();
|
||||
List<Pipe> functions = new ArrayList<>();
|
||||
functions.add(new DateTimeFormat(randomSource(), randomDatetimeLiteral(), randomStringLiteral(), randomZone())
|
||||
.makePipe());
|
||||
functions.add(new Format(randomSource(), randomDatetimeLiteral(), randomStringLiteral(), randomZone())
|
||||
.makePipe());
|
||||
return (DateTimeFormatPipe) randomFrom(functions);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -49,13 +54,23 @@ public class DateTimeFormatPipeTests extends AbstractNodeTestCase<DateTimeFormat
|
|||
DateTimeFormatPipe b1 = randomInstance();
|
||||
|
||||
Expression newExpression = randomValueOtherThan(b1.expression(), this::randomDateTimeFormatPipeExpression);
|
||||
DateTimeFormatPipe newB = new DateTimeFormatPipe(b1.source(), newExpression, b1.left(), b1.right(), b1.zoneId());
|
||||
DateTimeFormatPipe newB = new DateTimeFormatPipe(b1.source(), newExpression, b1.left(), b1.right(), b1.zoneId(), b1.formatter());
|
||||
assertEquals(newB, b1.transformPropertiesOnly(v -> Objects.equals(v, b1.expression()) ? newExpression : v, Expression.class));
|
||||
|
||||
DateTimeFormatPipe b2 = randomInstance();
|
||||
Source newLoc = randomValueOtherThan(b2.source(), SourceTests::randomSource);
|
||||
newB = new DateTimeFormatPipe(newLoc, b2.expression(), b2.left(), b2.right(), b2.zoneId());
|
||||
newB = new DateTimeFormatPipe(newLoc, b2.expression(), b2.left(), b2.right(), b2.zoneId(), b2.formatter());
|
||||
assertEquals(newB, b2.transformPropertiesOnly(v -> Objects.equals(v, b2.source()) ? newLoc : v, Source.class));
|
||||
|
||||
DateTimeFormatPipe b3 = randomInstance();
|
||||
Formatter newFormatter = randomValueOtherThan(b3.formatter(), () -> randomFrom(Formatter.values()));
|
||||
newB = new DateTimeFormatPipe(b3.source(), b3.expression(), b3.left(), b3.right(), b3.zoneId(), newFormatter);
|
||||
assertEquals(newB, b3.transformPropertiesOnly(v -> Objects.equals(v, b3.formatter()) ? newFormatter : v, Formatter.class));
|
||||
|
||||
DateTimeFormatPipe b4 = randomInstance();
|
||||
ZoneId newZI = randomValueOtherThan(b4.zoneId(), ESTestCase::randomZone);
|
||||
newB = new DateTimeFormatPipe(b4.source(), b4.expression(), b4.left(), b4.right(), newZI, b4.formatter());
|
||||
assertEquals(newB, b4.transformPropertiesOnly(v -> Objects.equals(v, b4.zoneId()) ? newZI : v, ZoneId.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -64,7 +79,7 @@ public class DateTimeFormatPipeTests extends AbstractNodeTestCase<DateTimeFormat
|
|||
Pipe newLeft = pipe(((Expression) randomValueOtherThan(b.left(), FunctionTestUtils::randomDatetimeLiteral)));
|
||||
Pipe newRight = pipe(((Expression) randomValueOtherThan(b.right(), FunctionTestUtils::randomStringLiteral)));
|
||||
ZoneId newZoneId = randomValueOtherThan(b.zoneId(), ESTestCase::randomZone);
|
||||
DateTimeFormatPipe newB = new DateTimeFormatPipe(b.source(), b.expression(), b.left(), b.right(), newZoneId);
|
||||
DateTimeFormatPipe newB = new DateTimeFormatPipe(b.source(), b.expression(), b.left(), b.right(), newZoneId, b.formatter());
|
||||
BinaryPipe transformed = newB.replaceChildren(newLeft, b.right());
|
||||
|
||||
assertEquals(transformed.left(), newLeft);
|
||||
|
@ -94,7 +109,8 @@ public class DateTimeFormatPipeTests extends AbstractNodeTestCase<DateTimeFormat
|
|||
f.expression(),
|
||||
pipe(((Expression) randomValueOtherThan(f.left(), FunctionTestUtils::randomDatetimeLiteral))),
|
||||
f.right(),
|
||||
randomValueOtherThan(f.zoneId(), ESTestCase::randomZone)
|
||||
randomValueOtherThan(f.zoneId(), ESTestCase::randomZone),
|
||||
f.formatter()
|
||||
)
|
||||
);
|
||||
randoms.add(
|
||||
|
@ -103,7 +119,8 @@ public class DateTimeFormatPipeTests extends AbstractNodeTestCase<DateTimeFormat
|
|||
f.expression(),
|
||||
f.left(),
|
||||
pipe(((Expression) randomValueOtherThan(f.right(), FunctionTestUtils::randomStringLiteral))),
|
||||
randomValueOtherThan(f.zoneId(), ESTestCase::randomZone)
|
||||
randomValueOtherThan(f.zoneId(), ESTestCase::randomZone),
|
||||
f.formatter()
|
||||
)
|
||||
);
|
||||
randoms.add(
|
||||
|
@ -112,7 +129,18 @@ public class DateTimeFormatPipeTests extends AbstractNodeTestCase<DateTimeFormat
|
|||
f.expression(),
|
||||
pipe(((Expression) randomValueOtherThan(f.left(), FunctionTestUtils::randomDatetimeLiteral))),
|
||||
pipe(((Expression) randomValueOtherThan(f.right(), FunctionTestUtils::randomStringLiteral))),
|
||||
randomValueOtherThan(f.zoneId(), ESTestCase::randomZone)
|
||||
randomValueOtherThan(f.zoneId(), ESTestCase::randomZone),
|
||||
f.formatter()
|
||||
)
|
||||
);
|
||||
randoms.add(
|
||||
f -> new DateTimeFormatPipe(
|
||||
f.source(),
|
||||
f.expression(),
|
||||
f.left(),
|
||||
f.right(),
|
||||
f.zoneId(),
|
||||
randomValueOtherThan(f.formatter(), () -> randomFrom(Formatter.values()))
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -121,6 +149,11 @@ public class DateTimeFormatPipeTests extends AbstractNodeTestCase<DateTimeFormat
|
|||
|
||||
@Override
|
||||
protected DateTimeFormatPipe copy(DateTimeFormatPipe instance) {
|
||||
return new DateTimeFormatPipe(instance.source(), instance.expression(), instance.left(), instance.right(), instance.zoneId());
|
||||
return new DateTimeFormatPipe(instance.source(),
|
||||
instance.expression(),
|
||||
instance.left(),
|
||||
instance.right(),
|
||||
instance.zoneId(),
|
||||
instance.formatter());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,10 +13,12 @@ import org.elasticsearch.xpack.ql.expression.gen.processor.ConstantProcessor;
|
|||
import org.elasticsearch.xpack.ql.tree.Source;
|
||||
import org.elasticsearch.xpack.sql.AbstractSqlWireSerializingTestCase;
|
||||
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
|
||||
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFormatProcessor.Formatter;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import static org.elasticsearch.xpack.ql.expression.Literal.NULL;
|
||||
import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTestUtils.l;
|
||||
|
@ -32,7 +34,8 @@ public class DateTimeFormatProcessorTests extends AbstractSqlWireSerializingTest
|
|||
return new DateTimeFormatProcessor(
|
||||
new ConstantProcessor(DateTimeTestUtils.nowWithMillisResolution()),
|
||||
new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 128)),
|
||||
randomZone()
|
||||
randomZone(),
|
||||
randomFrom(Formatter.values())
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -60,11 +63,12 @@ public class DateTimeFormatProcessorTests extends AbstractSqlWireSerializingTest
|
|||
return new DateTimeFormatProcessor(
|
||||
new ConstantProcessor(DateTimeTestUtils.nowWithMillisResolution()),
|
||||
new ConstantProcessor(ESTestCase.randomRealisticUnicodeOfLength(128)),
|
||||
randomValueOtherThan(instance.zoneId(), ESTestCase::randomZone)
|
||||
randomZone(),
|
||||
randomValueOtherThan(instance.formatter(), () -> randomFrom(Formatter.values()))
|
||||
);
|
||||
}
|
||||
|
||||
public void testInvalidInputs() {
|
||||
public void testDateTimeFormatInvalidInputs() {
|
||||
SqlIllegalArgumentException siae = expectThrows(
|
||||
SqlIllegalArgumentException.class,
|
||||
() -> new DateTimeFormat(Source.EMPTY, l("foo"), randomStringLiteral(), randomZone()).makePipe().asProcessor().process(null)
|
||||
|
@ -100,10 +104,50 @@ public class DateTimeFormatProcessorTests extends AbstractSqlWireSerializingTest
|
|||
);
|
||||
}
|
||||
|
||||
public void testFormatInvalidInputs() {
|
||||
SqlIllegalArgumentException siae = expectThrows(
|
||||
SqlIllegalArgumentException.class,
|
||||
() -> new Format(Source.EMPTY, l("foo"), randomStringLiteral(), randomZone()).makePipe().asProcessor().process(null)
|
||||
);
|
||||
assertEquals("A date/datetime/time is required; received [foo]", siae.getMessage());
|
||||
|
||||
siae = expectThrows(
|
||||
SqlIllegalArgumentException.class,
|
||||
() -> new Format(Source.EMPTY, randomDatetimeLiteral(), l(5), randomZone()).makePipe().asProcessor().process(null)
|
||||
);
|
||||
assertEquals("A string is required; received [5]", siae.getMessage());
|
||||
|
||||
siae = expectThrows(
|
||||
SqlIllegalArgumentException.class,
|
||||
() -> new Format(Source.EMPTY, l(dateTime(2019, 9, 3, 18, 10, 37, 0)), l("invalid"), randomZone()).makePipe()
|
||||
.asProcessor()
|
||||
.process(null)
|
||||
);
|
||||
assertEquals(
|
||||
"Invalid pattern [invalid] is received for formatting date/time [2019-09-03T18:10:37Z]; Unknown pattern letter: i",
|
||||
siae.getMessage()
|
||||
);
|
||||
|
||||
siae = expectThrows(
|
||||
SqlIllegalArgumentException.class,
|
||||
() -> new Format(Source.EMPTY, l(time(18, 10, 37, 123000000)), l("MM/dd"), randomZone()).makePipe()
|
||||
.asProcessor()
|
||||
.process(null)
|
||||
);
|
||||
assertEquals(
|
||||
"Invalid pattern [MM/dd] is received for formatting date/time [18:10:37.123Z]; Unsupported field: MonthOfYear",
|
||||
siae.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
public void testWithNulls() {
|
||||
assertNull(new DateTimeFormat(Source.EMPTY, randomDatetimeLiteral(), NULL, randomZone()).makePipe().asProcessor().process(null));
|
||||
assertNull(new DateTimeFormat(Source.EMPTY, randomDatetimeLiteral(), l(""), randomZone()).makePipe().asProcessor().process(null));
|
||||
assertNull(new DateTimeFormat(Source.EMPTY, NULL, randomStringLiteral(), randomZone()).makePipe().asProcessor().process(null));
|
||||
|
||||
assertNull(new Format(Source.EMPTY, randomDatetimeLiteral(), NULL, randomZone()).makePipe().asProcessor().process(null));
|
||||
assertNull(new Format(Source.EMPTY, randomDatetimeLiteral(), l(""), randomZone()).makePipe().asProcessor().process(null));
|
||||
assertNull(new Format(Source.EMPTY, NULL, randomStringLiteral(), randomZone()).makePipe().asProcessor().process(null));
|
||||
}
|
||||
|
||||
public void testFormatting() {
|
||||
|
@ -141,5 +185,111 @@ public class DateTimeFormatProcessorTests extends AbstractSqlWireSerializingTest
|
|||
.asProcessor()
|
||||
.process(null)
|
||||
);
|
||||
|
||||
|
||||
zoneId = ZoneId.of("Etc/GMT-10");
|
||||
dateTime = l(dateTime(2019, 9, 3, 18, 10, 37, 123456789));
|
||||
|
||||
assertEquals("AD : 3", new Format(Source.EMPTY, dateTime, l("G : Q"), zoneId).makePipe().asProcessor().process(null));
|
||||
assertEquals("AD", new Format(Source.EMPTY, dateTime, l("g"), zoneId).makePipe().asProcessor().process(null));
|
||||
assertEquals(
|
||||
"2019-09-04",
|
||||
new Format(Source.EMPTY, dateTime, l("YYYY-MM-dd"), zoneId).makePipe().asProcessor().process(null)
|
||||
);
|
||||
assertEquals(
|
||||
"2019-09-04 Wed",
|
||||
new Format(Source.EMPTY, dateTime, l("YYYY-MM-dd ddd"), zoneId).makePipe().asProcessor().process(null)
|
||||
);
|
||||
assertEquals(
|
||||
"2019-09-04 Wednesday",
|
||||
new Format(Source.EMPTY, dateTime, l("YYYY-MM-dd dddd"), zoneId).makePipe().asProcessor().process(null)
|
||||
);
|
||||
assertEquals(
|
||||
"04:10:37.123456",
|
||||
new Format(Source.EMPTY, dateTime, l("HH:mm:ss.ffffff"), zoneId).makePipe().asProcessor().process(null)
|
||||
);
|
||||
assertEquals(
|
||||
"2019-09-04 04:10:37.12345678",
|
||||
new Format(Source.EMPTY, dateTime, l("YYYY-MM-dd HH:mm:ss.ffffffff"), zoneId).makePipe().asProcessor().process(null)
|
||||
);
|
||||
assertEquals(
|
||||
"2019-09-04 04:10:37.12345678 AM",
|
||||
new Format(Source.EMPTY, dateTime, l("YYYY-MM-dd HH:mm:ss.ffffffff tt"), zoneId).makePipe().asProcessor().process(null)
|
||||
);
|
||||
assertEquals(
|
||||
"2019-09-04 04:10:37.12345678 AM",
|
||||
new Format(Source.EMPTY, dateTime, l("YYYY-MM-dd HH:mm:ss.ffffffff t"), zoneId).makePipe().asProcessor().process(null)
|
||||
);
|
||||
assertEquals("+1000", new Format(Source.EMPTY, dateTime, l("Z"), zoneId).makePipe().asProcessor().process(null));
|
||||
assertEquals("+10", new Format(Source.EMPTY, dateTime, l("z"), zoneId).makePipe().asProcessor().process(null));
|
||||
assertEquals("Etc/GMT-10", new Format(Source.EMPTY, dateTime, l("VV"), zoneId).makePipe().asProcessor().process(null));
|
||||
assertEquals("Etc/GMT-10", new Format(Source.EMPTY, dateTime, l("KK"), zoneId).makePipe().asProcessor().process(null));
|
||||
|
||||
assertEquals("1", new Format(Source.EMPTY, dateTime, l("F"), zoneId).makePipe().asProcessor().process(null));
|
||||
assertEquals("12", new Format(Source.EMPTY, dateTime, l("FF"), zoneId).makePipe().asProcessor().process(null));
|
||||
|
||||
zoneId = ZoneId.of("America/Sao_Paulo");
|
||||
assertEquals("-0300", new Format(Source.EMPTY, dateTime, l("Z"), zoneId).makePipe().asProcessor().process(null));
|
||||
assertEquals("-03", new Format(Source.EMPTY, dateTime, l("z"), zoneId).makePipe().asProcessor().process(null));
|
||||
assertEquals(
|
||||
"America/Sao_Paulo",
|
||||
new Format(Source.EMPTY, dateTime, l("VV"), zoneId).makePipe().asProcessor().process(null)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"07:11:22.1234",
|
||||
new Format(Source.EMPTY, l(time(10, 11, 22, 123456789), TIME), l("HH:mm:ss.ffff"), zoneId).makePipe()
|
||||
.asProcessor()
|
||||
.process(null)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"10:11",
|
||||
new Format(Source.EMPTY, l(time(10, 11, 22, 123456789), TIME), l("H:m"), ZoneOffset.UTC).makePipe()
|
||||
.asProcessor()
|
||||
.process(null)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"21:9",
|
||||
new Format(Source.EMPTY, l(time(21, 11, 22, 123456789), TIME), l("H:h"), ZoneOffset.UTC).makePipe()
|
||||
.asProcessor()
|
||||
.process(null)
|
||||
);
|
||||
assertEquals(
|
||||
"2-02",
|
||||
new Format(Source.EMPTY, l(time(21, 11, 2, 123456789), TIME), l("s-ss"), ZoneOffset.UTC).makePipe()
|
||||
.asProcessor()
|
||||
.process(null)
|
||||
);
|
||||
|
||||
assertEquals("9-09-Sep-September",
|
||||
new Format(Source.EMPTY, dateTime, l("M-MM-MMM-MMMM"), zoneId).makePipe()
|
||||
.asProcessor()
|
||||
.process(null));
|
||||
|
||||
assertEquals("arr: 3:10 PM",
|
||||
new Format(Source.EMPTY, dateTime, l("'arr:' h:m t"), zoneId).makePipe()
|
||||
.asProcessor()
|
||||
.process(null))
|
||||
;
|
||||
assertEquals("-03/-0300/-03:00",
|
||||
new Format(Source.EMPTY, dateTime, l("z/zz/zzz"), zoneId).makePipe()
|
||||
.asProcessor()
|
||||
.process(null));
|
||||
assertEquals("3", new Format(Source.EMPTY, dateTime, l("d"), zoneId).makePipe().asProcessor().process(null));
|
||||
assertEquals("2001-01-2001-02001",
|
||||
new Format(Source.EMPTY, l(dateTime(2001, 9, 3, 18, 10, 37, 123456789)),
|
||||
l("y-yy-yyyy-yyyyy"), zoneId).makePipe().asProcessor().process(null));
|
||||
|
||||
assertEquals("%9-\"09-\\Sep-September",
|
||||
new Format(Source.EMPTY, dateTime, l("%M-\"MM-\\MMM-MMMM"), zoneId).makePipe()
|
||||
.asProcessor()
|
||||
.process(null));
|
||||
|
||||
assertEquals("45-0045",
|
||||
new Format(Source.EMPTY, l(dateTime(45, 9, 3, 18, 10, 37, 123456789)), l("y-yyyy"), zoneId).makePipe()
|
||||
.asProcessor()
|
||||
.process(null));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -560,6 +560,22 @@ public class QueryTranslatorTests extends ESTestCase {
|
|||
assertEquals("[{v=keyword}, {v=uuuu_MM_dd}, {v=Z}, {v=2018-09-04T00:00:00.000Z}]", sc.script().params().toString());
|
||||
}
|
||||
|
||||
public void testTranslateFormat_WhereClause_Painless() {
|
||||
LogicalPlan p = plan("SELECT int FROM test WHERE FORMAT(date, 'YYYY_MM_dd') = '2018_09_04'");
|
||||
assertTrue(p instanceof Project);
|
||||
assertTrue(p.children().get(0) instanceof Filter);
|
||||
Expression condition = ((Filter) p.children().get(0)).condition();
|
||||
assertFalse(condition.foldable());
|
||||
QueryTranslation translation = QueryTranslator.toQuery(condition, false);
|
||||
assertNull(translation.aggFilter);
|
||||
assertTrue(translation.query instanceof ScriptQuery);
|
||||
ScriptQuery sc = (ScriptQuery) translation.query;
|
||||
assertEquals("InternalQlScriptUtils.nullSafeFilter(InternalQlScriptUtils.eq(InternalSqlScriptUtils.format(" +
|
||||
"InternalQlScriptUtils.docValue(doc,params.v0),params.v1,params.v2),params.v3))",
|
||||
sc.script().toString());
|
||||
assertEquals("[{v=date}, {v=YYYY_MM_dd}, {v=Z}, {v=2018_09_04}]", sc.script().params().toString());
|
||||
}
|
||||
|
||||
public void testLikeOnInexact() {
|
||||
LogicalPlan p = plan("SELECT * FROM test WHERE some.string LIKE '%a%'");
|
||||
assertTrue(p instanceof Project);
|
||||
|
|
Loading…
Reference in New Issue