diff --git a/docs/reference/sql/functions/date-time.asciidoc b/docs/reference/sql/functions/date-time.asciidoc index 2d8139624cd..3b254d0c548 100644 --- a/docs/reference/sql/functions/date-time.asciidoc +++ b/docs/reference/sql/functions/date-time.asciidoc @@ -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 - <>/<> REST/driver + <>/<> 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` diff --git a/docs/reference/sql/functions/index.asciidoc b/docs/reference/sql/functions/index.asciidoc index 1b5b8203f13..4d426b5af97 100644 --- a/docs/reference/sql/functions/index.asciidoc +++ b/docs/reference/sql/functions/index.asciidoc @@ -58,6 +58,7 @@ ** <> ** <> ** <> +** <> ** <> ** <> ** <> diff --git a/x-pack/plugin/sql/qa/server/src/main/resources/command.csv-spec b/x-pack/plugin/sql/qa/server/src/main/resources/command.csv-spec index 39604f1edf4..ec403ab44e1 100644 --- a/x-pack/plugin/sql/qa/server/src/main/resources/command.csv-spec +++ b/x-pack/plugin/sql/qa/server/src/main/resources/command.csv-spec @@ -66,6 +66,7 @@ DAY_OF_YEAR |SCALAR DOM |SCALAR DOW |SCALAR DOY |SCALAR +FORMAT |SCALAR HOUR |SCALAR HOUR_OF_DAY |SCALAR IDOW |SCALAR diff --git a/x-pack/plugin/sql/qa/server/src/main/resources/datetime.csv-spec b/x-pack/plugin/sql/qa/server/src/main/resources/datetime.csv-spec index f431b9b8b5e..dd2dd251fc3 100644 --- a/x-pack/plugin/sql/qa/server/src/main/resources/datetime.csv-spec +++ b/x-pack/plugin/sql/qa/server/src/main/resources/datetime.csv-spec @@ -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 // diff --git a/x-pack/plugin/sql/qa/server/src/main/resources/docs/docs.csv-spec b/x-pack/plugin/sql/qa/server/src/main/resources/docs/docs.csv-spec index e27ff4b366c..7a43701f1d6 100644 --- a/x-pack/plugin/sql/qa/server/src/main/resources/docs/docs.csv-spec +++ b/x-pack/plugin/sql/qa/server/src/main/resources/docs/docs.csv-spec @@ -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; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/SqlFunctionRegistry.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/SqlFunctionRegistry.java index 6060d024d5b..ec15ce5c6b2 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/SqlFunctionRegistry.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/SqlFunctionRegistry.java @@ -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"), diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeFormatFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeFormatFunction.java new file mode 100644 index 00000000000..48f54039136 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/BaseDateTimeFormatFunction.java @@ -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 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 ctor(); +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormat.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormat.java index 7a3d63d14a0..1ad390855cd 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormat.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormat.java @@ -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 info() { - return NodeInfo.create(this, DateTimeFormat::new, left(), right(), zoneId()); + protected NodeInfo.NodeCtor3 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()); } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatPipe.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatPipe.java index bd7390024a4..bd1ff256884 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatPipe.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatPipe.java @@ -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 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; } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatProcessor.java index 769d9f12f65..fc7885a8f16 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatProcessor.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatProcessor.java @@ -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; } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/Format.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/Format.java new file mode 100644 index 00000000000..4d031ef96b6 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/Format.java @@ -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 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()); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java index a429a41d7e8..345e417cd2d 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/whitelist/InternalSqlScriptUtils.java @@ -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); } diff --git a/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt b/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt index f2c10c5361a..555fdd18deb 100644 --- a/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt +++ b/x-pack/plugin/sql/src/main/resources/org/elasticsearch/xpack/sql/plugin/sql_whitelist.txt @@ -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) diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java index a6bdb4d36e7..6c395c11f51 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java @@ -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"); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatPipeTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatPipeTests.java index d340c47dadb..c2ca7d0ed24 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatPipeTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeFormatPipeTests.java @@ -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 { public static DateTimeFormatPipe randomDateTimeFormatPipe() { - return (DateTimeFormatPipe) new DateTimeFormat(randomSource(), randomDatetimeLiteral(), randomStringLiteral(), randomZone()) - .makePipe(); + List 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 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 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 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)); } } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java index 6153a6911c7..cbcdb64134c 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java @@ -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);