Skip to content

Commit 1f612cc

Browse files
matrivbpinteaastefan
authored
SQL: Implement FORMAT function (#55454) (#62701)
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 <[email protected]> Co-authored-by: Bogdan Pintea <[email protected]> Co-authored-by: Andrei Stefan <[email protected]> (cherry picked from commit da511f4)
1 parent cadd5dc commit 1f612cc

File tree

17 files changed

+686
-93
lines changed

17 files changed

+686
-93
lines changed

docs/reference/sql/functions/date-time.asciidoc

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -579,9 +579,9 @@ include-tagged::{sql-specs}/docs/docs.csv-spec[timeParse2]
579579

580580
[NOTE]
581581
====
582-
If timezone is not specified in the time string expression and the parsing pattern,
582+
If timezone is not specified in the time string expression and the parsing pattern,
583583
the resulting `time` will have the offset of the time zone specified by the user through the
584-
<<sql-rest-fields-timezone,`time_zone`>>/<<jdbc-cfg-timezone,`timezone`>> REST/driver
584+
<<sql-rest-fields-timezone,`time_zone`>>/<<jdbc-cfg-timezone,`timezone`>> REST/driver
585585
parameters at the Unix epoch date (`1970-01-01`) with no conversion applied.
586586
587587
[source, sql]
@@ -765,6 +765,59 @@ include-tagged::{sql-specs}/docs/docs.csv-spec[truncateIntervalHour]
765765
include-tagged::{sql-specs}/docs/docs.csv-spec[truncateIntervalDay]
766766
--------------------------------------------------
767767

768+
[[sql-functions-datetime-format]]
769+
==== `FORMAT`
770+
771+
.Synopsis:
772+
[source, sql]
773+
--------------------------------------------------
774+
FORMAT(
775+
date_exp/datetime_exp/time_exp, <1>
776+
string_exp) <2>
777+
--------------------------------------------------
778+
779+
*Input*:
780+
781+
<1> date/datetime/time expression
782+
<2> format pattern
783+
784+
*Output*: string
785+
786+
*Description*: Returns the date/datetime/time as a string using the
787+
https://docs.microsoft.com/en-us/sql/t-sql/functions/format-transact-sql#arguments[format] specified in the 2nd argument. The formatting
788+
pattern used is the one from
789+
https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings[Microsoft SQL Server Format Specification].
790+
If any of the two arguments is `null` or the pattern is an empty string `null` is returned.
791+
792+
[NOTE]
793+
If the 1st argument is of type `time`, then pattern specified by the 2nd argument cannot contain date related units
794+
(e.g. 'dd', 'MM', 'YYYY', etc.). If it contains such units an error is returned.
795+
796+
*Special Cases*
797+
798+
- Format specifier `F` will be working similar to format specifier `f`.
799+
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).
800+
Result will contain `0` appended in the end to match with number of `F` provided.
801+
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`.
802+
- Format Specifier `y` will return year-of-era instead of one/two low-order digits.
803+
eg.: For year `2009`, `y` will be returning `2009` instead of `9`. For year `43`, `y` format specifier will return `43`.
804+
- Special characters like `"` , `\` and `%` will be returned as it is without any change. eg.: formatting date `17-sep-2020` with `%M` will return `%9`
805+
806+
[source, sql]
807+
--------------------------------------------------
808+
include-tagged::{sql-specs}/docs/docs.csv-spec[formatDate]
809+
--------------------------------------------------
810+
811+
[source, sql]
812+
--------------------------------------------------
813+
include-tagged::{sql-specs}/docs/docs.csv-spec[formatDateTime]
814+
--------------------------------------------------
815+
816+
[source, sql]
817+
--------------------------------------------------
818+
include-tagged::{sql-specs}/docs/docs.csv-spec[formatTime]
819+
--------------------------------------------------
820+
768821
[[sql-functions-datetime-day]]
769822
==== `DAY_OF_MONTH/DOM/DAY`
770823

docs/reference/sql/functions/index.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
** <<sql-functions-datetime-dateparse>>
5959
** <<sql-functions-datetime-datetimeformat>>
6060
** <<sql-functions-datetime-datetimeparse>>
61+
** <<sql-functions-datetime-format>>
6162
** <<sql-functions-datetime-timeparse>>
6263
** <<sql-functions-datetime-part>>
6364
** <<sql-functions-datetime-trunc>>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ DAY_OF_YEAR |SCALAR
6666
DOM |SCALAR
6767
DOW |SCALAR
6868
DOY |SCALAR
69+
FORMAT |SCALAR
6970
HOUR |SCALAR
7071
HOUR_OF_DAY |SCALAR
7172
IDOW |SCALAR

x-pack/plugin/sql/qa/server/src/main/resources/datetime.csv-spec

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,120 @@ F | 1997-05-19 00:00:00.000Z
10171017
M | 1996-11-05 00:00:00.000Z
10181018
;
10191019

1020+
selectFormat
1021+
schema::format_date:s|format_datetime:s|format_time:s
1022+
SELECT FORMAT('2020-04-05T11:22:33.123Z'::date, 'dd/MM/YYYY HH:mm:ss.fff') AS format_date,
1023+
FORMAT('2020-04-05T11:22:33.123Z'::datetime, 'dd/MM/YYYY HH:mm:ss.ff') AS format_datetime,
1024+
FORMAT('11:22:33.123456789Z'::time, 'HH:mm:ss.ff') AS format_time;
1025+
1026+
format_date | format_datetime | format_time
1027+
------------------------+------------------------+----------------
1028+
05/04/2020 00:00:00.000 | 05/04/2020 11:22:33.12 | 11:22:33.12
1029+
;
1030+
1031+
selectFormatWithLength
1032+
schema::format_datetime:s|length:i
1033+
SELECT FORMAT('2020-04-05T11:22:33.123Z'::datetime, 'dd/MM/YYYY HH:mm:ss.ff') AS format_datetime,
1034+
LENGTH(FORMAT('2020-04-05T11:22:33.123Z'::datetime, 'dd/MM/YYYY HH:mm:ss.ff')) AS length;
1035+
1036+
format_datetime | length
1037+
------------------------+----------------
1038+
05/04/2020 11:22:33.12 | 22
1039+
;
1040+
1041+
selectFormatWithField
1042+
schema::birth_date:ts|format_birth_date1:s|format_birth_date2:s|emp_no:i
1043+
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
1044+
FROM test_emp WHERE gender = 'M' AND emp_no BETWEEN 10037 AND 10052 ORDER BY emp_no;
1045+
1046+
birth_date | format_birth_date1 | format_birth_date2 | emp_no
1047+
-------------------------+--------------------+--------------------+----------
1048+
1963-07-22 00:00:00.000Z | 07/22/1963 | 07/22 | 10037
1049+
1960-07-20 00:00:00.000Z | 07/20/1960 | 07/20 | 10038
1050+
1959-10-01 00:00:00.000Z | 10/01/1959 | 10/01 | 10039
1051+
null | null | null | 10043
1052+
null | null | null | 10045
1053+
null | null | null | 10046
1054+
null | null | null | 10047
1055+
null | null | null | 10048
1056+
1958-05-21 00:00:00.000Z | 05/21/1958 | 05/21 | 10050
1057+
1953-07-28 00:00:00.000Z | 07/28/1953 | 07/28 | 10051
1058+
1961-02-26 00:00:00.000Z | 02/26/1961 | 02/26 | 10052
1059+
;
1060+
1061+
formatWhere
1062+
schema::birth_date:ts|format_birth_date:s|emp_no:i
1063+
SELECT birth_date, FORMAT(birth_date, 'MM') AS format_birth_date, emp_no FROM test_emp
1064+
WHERE FORMAT(birth_date, 'MM')::integer > 10 ORDER BY emp_no LIMIT 10;
1065+
1066+
birth_date | format_birth_date | emp_no
1067+
-------------------------+-------------------+----------
1068+
1959-12-03 00:00:00.000Z | 12 | 10003
1069+
1953-11-07 00:00:00.000Z | 11 | 10011
1070+
1952-12-24 00:00:00.000Z | 12 | 10020
1071+
1963-11-26 00:00:00.000Z | 11 | 10028
1072+
1956-12-13 00:00:00.000Z | 12 | 10029
1073+
1956-11-14 00:00:00.000Z | 11 | 10033
1074+
1962-12-29 00:00:00.000Z | 12 | 10034
1075+
1961-11-02 00:00:00.000Z | 11 | 10062
1076+
1952-11-13 00:00:00.000Z | 11 | 10066
1077+
1962-11-26 00:00:00.000Z | 11 | 10068
1078+
;
1079+
1080+
formatOrderBy
1081+
schema::birth_date:ts|format_birth_date:s
1082+
SELECT birth_date, FORMAT(birth_date, 'MM/dd/YYYY') AS format_birth_date FROM test_emp ORDER BY 2 DESC NULLS LAST LIMIT 10;
1083+
1084+
birth_date | format_birth_date
1085+
-------------------------+---------------
1086+
1962-12-29 00:00:00.000Z | 12/29/1962
1087+
1959-12-25 00:00:00.000Z | 12/25/1959
1088+
1952-12-24 00:00:00.000Z | 12/24/1952
1089+
1960-12-17 00:00:00.000Z | 12/17/1960
1090+
1956-12-13 00:00:00.000Z | 12/13/1956
1091+
1959-12-03 00:00:00.000Z | 12/03/1959
1092+
1957-12-03 00:00:00.000Z | 12/03/1957
1093+
1963-11-26 00:00:00.000Z | 11/26/1963
1094+
1962-11-26 00:00:00.000Z | 11/26/1962
1095+
1962-11-19 00:00:00.000Z | 11/19/1962
1096+
;
1097+
1098+
formatGroupBy
1099+
schema::count:l|format_birth_date:s
1100+
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;
1101+
1102+
count | format_birth_date
1103+
-------+---------------
1104+
10 | 09
1105+
10 | 05
1106+
10 | null
1107+
9 | 10
1108+
9 | 07
1109+
8 | 11
1110+
8 | 04
1111+
8 | 02
1112+
7 | 12
1113+
7 | 06
1114+
6 | 08
1115+
6 | 01
1116+
2 | 03
1117+
;
1118+
1119+
formatHaving
1120+
schema::max:ts|format_birth_date:s
1121+
SELECT MAX(birth_date) AS max, FORMAT(birth_date, 'MM') AS format_birth_date FROM test_emp GROUP BY format_birth_date
1122+
HAVING FORMAT(MAX(birth_date), 'dd')::integer > 20 ORDER BY 1 DESC;
1123+
1124+
max | format_birth_date
1125+
-------------------------+---------------
1126+
1963-11-26 00:00:00.000Z | 11
1127+
1963-07-22 00:00:00.000Z | 07
1128+
1963-03-21 00:00:00.000Z | 03
1129+
1962-12-29 00:00:00.000Z | 12
1130+
1961-05-30 00:00:00.000Z | 05
1131+
1961-02-26 00:00:00.000Z | 02
1132+
;
1133+
10201134
//
10211135
// Aggregate
10221136
//

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ DAY_OF_YEAR |SCALAR
262262
DOM |SCALAR
263263
DOW |SCALAR
264264
DOY |SCALAR
265+
FORMAT |SCALAR
265266
HOUR |SCALAR
266267
HOUR_OF_DAY |SCALAR
267268
IDOW |SCALAR
@@ -3088,6 +3089,36 @@ SELECT DATE_TRUNC('days', INTERVAL '19 15:24:19' DAY TO SECONDS) AS day;
30883089
// end::truncateIntervalDay
30893090
;
30903091

3092+
formatDate
3093+
// tag::formatDate
3094+
SELECT FORMAT(CAST('2020-04-05' AS DATE), 'dd/MM/YYYY') AS "date";
3095+
3096+
date
3097+
------------------
3098+
05/04/2020
3099+
// end::formatDate
3100+
;
3101+
3102+
formatDateTime
3103+
// tag::formatDateTime
3104+
SELECT FORMAT(CAST('2020-04-05T11:22:33.987654' AS DATETIME), 'dd/MM/YYYY HH:mm:ss.ff') AS "datetime";
3105+
3106+
datetime
3107+
------------------
3108+
05/04/2020 11:22:33.98
3109+
// end::formatDateTime
3110+
;
3111+
3112+
formatTime
3113+
// tag::formatTime
3114+
SELECT FORMAT(CAST('11:22:33.987' AS TIME), 'HH mm ss.f') AS "time";
3115+
3116+
time
3117+
------------------
3118+
11 22 33.9
3119+
// end::formatTime
3120+
;
3121+
30913122
constantDayOfWeek
30923123
// tag::dayOfWeek
30933124
SELECT DAY_OF_WEEK(CAST('2018-02-19T10:23:27Z' AS TIMESTAMP)) AS day;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayOfMonth;
4444
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayOfWeek;
4545
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayOfYear;
46+
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.Format;
4647
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.HourOfDay;
4748
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.IsoDayOfWeek;
4849
import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.IsoWeekOfYear;
@@ -182,6 +183,7 @@ private static FunctionDefinition[][] functions() {
182183
def(DateTimeFormat.class, DateTimeFormat::new, "DATETIME_FORMAT"),
183184
def(DateTimeParse.class, DateTimeParse::new, "DATETIME_PARSE"),
184185
def(DateTrunc.class, DateTrunc::new, "DATETRUNC", "DATE_TRUNC"),
186+
def(Format.class, Format::new, "FORMAT"),
185187
def(HourOfDay.class, HourOfDay::new, "HOUR_OF_DAY", "HOUR"),
186188
def(IsoDayOfWeek.class, IsoDayOfWeek::new, "ISO_DAY_OF_WEEK", "ISODAYOFWEEK", "ISODOW", "IDOW"),
187189
def(IsoWeekOfYear.class, IsoWeekOfYear::new, "ISO_WEEK_OF_YEAR", "ISOWEEKOFYEAR", "ISOWEEK", "IWOY", "IW"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.sql.expression.function.scalar.datetime;
8+
9+
import org.elasticsearch.xpack.ql.expression.Expression;
10+
import org.elasticsearch.xpack.ql.expression.Expressions;
11+
import org.elasticsearch.xpack.ql.expression.gen.pipeline.Pipe;
12+
import org.elasticsearch.xpack.ql.tree.NodeInfo;
13+
import org.elasticsearch.xpack.ql.tree.NodeInfo.NodeCtor3;
14+
import org.elasticsearch.xpack.ql.tree.Source;
15+
import org.elasticsearch.xpack.ql.type.DataType;
16+
import org.elasticsearch.xpack.ql.type.DataTypes;
17+
18+
import java.time.ZoneId;
19+
20+
import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isString;
21+
import static org.elasticsearch.xpack.sql.expression.SqlTypeResolutions.isDateOrTime;
22+
import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFormatProcessor.Formatter;
23+
24+
public abstract class BaseDateTimeFormatFunction extends BinaryDateTimeFunction {
25+
public BaseDateTimeFormatFunction(Source source, Expression timestamp, Expression pattern, ZoneId zoneId) {
26+
super(source, timestamp, pattern, zoneId);
27+
}
28+
29+
@Override
30+
public DataType dataType() {
31+
return DataTypes.KEYWORD;
32+
}
33+
34+
@Override
35+
protected TypeResolution resolveType() {
36+
TypeResolution resolution = isDateOrTime(left(), sourceText(), Expressions.ParamOrdinal.FIRST);
37+
if (resolution.unresolved()) {
38+
return resolution;
39+
}
40+
resolution = isString(right(), sourceText(), Expressions.ParamOrdinal.SECOND);
41+
if (resolution.unresolved()) {
42+
return resolution;
43+
}
44+
return TypeResolution.TYPE_RESOLVED;
45+
}
46+
47+
@Override
48+
protected NodeInfo<? extends Expression> info() {
49+
return NodeInfo.create(this, ctor(), left(), right(), zoneId());
50+
}
51+
52+
@Override
53+
public Object fold() {
54+
return formatter().format(left().fold(), right().fold(), zoneId());
55+
}
56+
57+
@Override
58+
protected Pipe createPipe(Pipe timestamp, Pipe pattern, ZoneId zoneId) {
59+
return new DateTimeFormatPipe(source(), this, timestamp, pattern, zoneId, formatter());
60+
}
61+
62+
protected abstract Formatter formatter();
63+
64+
protected abstract NodeCtor3<Expression, Expression, ZoneId, BaseDateTimeFormatFunction> ctor();
65+
}

0 commit comments

Comments
 (0)