From 59b3294bc983488c7b17485430dbb39c8bafb6ee Mon Sep 17 00:00:00 2001 From: Marios Trivyzas Date: Thu, 10 Oct 2019 15:24:36 +0200 Subject: [PATCH] SQL: Implement DATEADD function (#47747) Implement DATEADD/TIMESTAMPADD function as per the MS-SQL spec: https://docs.microsoft.com/en-us/sql/t-sql/functions/dateadd-transact-sql?view=sql-server-2017 which allows a user to add/subtract specified number of specified units to/from a date/datetime field/expression. Closes: #47746 (cherry picked from commit e624bc281bebb4bbe0b0c2e0a8cbc712e50097a8) --- .../sql/functions/date-time.asciidoc | 73 +++++++ docs/reference/sql/functions/index.asciidoc | 1 + .../qa/src/main/resources/command.csv-spec | 6 +- .../qa/src/main/resources/datetime.csv-spec | 154 ++++++++++++++ .../qa/src/main/resources/docs/docs.csv-spec | 56 ++++- .../expression/function/FunctionRegistry.java | 28 ++- .../function/scalar/Processors.java | 2 + .../function/scalar/datetime/DateAdd.java | 173 +++++++++++++++ .../function/scalar/datetime/DateAddPipe.java | 36 ++++ .../scalar/datetime/DateAddProcessor.java | 72 +++++++ .../datetime/ThreeArgsDateTimeFunction.java | 127 +++++++++++ .../datetime/ThreeArgsDateTimePipe.java | 86 ++++++++ .../datetime/ThreeArgsDateTimeProcessor.java | 98 +++++++++ .../whitelist/InternalSqlScriptUtils.java | 13 +- .../xpack/sql/plugin/sql_whitelist.txt | 1 + .../analyzer/VerifierErrorMessagesTests.java | 28 +++ .../scalar/datetime/DateAddPipeTests.java | 152 +++++++++++++ .../datetime/DateAddProcessorTests.java | 201 ++++++++++++++++++ .../sql/planner/QueryTranslatorTests.java | 99 +++++---- 19 files changed, 1357 insertions(+), 49 deletions(-) create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAdd.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddPipe.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddProcessor.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimeFunction.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimePipe.java create mode 100644 x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimeProcessor.java create mode 100644 x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddPipeTests.java create mode 100644 x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddProcessorTests.java diff --git a/docs/reference/sql/functions/date-time.asciidoc b/docs/reference/sql/functions/date-time.asciidoc index ea099aa470a..e7d52670502 100644 --- a/docs/reference/sql/functions/date-time.asciidoc +++ b/docs/reference/sql/functions/date-time.asciidoc @@ -248,6 +248,79 @@ include-tagged::{sql-specs}/docs/docs.csv-spec[filterNow] Currently, using a _precision_ greater than 3 doesn't make any difference to the output of the function as the maximum number of second fractional digits returned is 3 (milliseconds). +[[sql-functions-datetime-add]] +==== `DATE_ADD/DATEADD/TIMESTAMPADD/TIMESTAMP_ADD` + +.Synopsis: +[source, sql] +-------------------------------------------------- +DATE_ADD( + string_exp, <1> + integer_exp, <2> + datetime_exp) <3> +-------------------------------------------------- + +*Input*: + +<1> string expression denoting the date/time unit to add to the date/datetime +<2> integer expression denoting how many times the above unit should be added to/from the date/datetime, +if a negative value is used it results to a subtraction from the date/datetime +<3> date/datetime expression + +*Output*: datetime + +.Description: + +Add the given number of date/time units to a date/datetime. If the number of units is negative then it's subtracted from +the date/datetime. If any of the three arguments is `null` a `null` is returned. + +[cols="^,^"] +|=== +2+h|Datetime units to add/subtract + +s|unit +s|abbreviations + +| year | years, yy, yyyy +| quarter | quarters, qq, q +| month | months, mm, m +| dayofyear | dy, y +| day | days, dd, d +| week | weeks, wk, ww +| weekday | weekdays, dw +| hour | hours, hh +| minute | minutes, mi, n +| second | seconds, ss, s +| millisecond | milliseconds, ms +| microsecond | microseconds, mcs +| nanosecond | nanoseconds, ns +|=== + + +[source, sql] +-------------------------------------------------- +include-tagged::{sql-specs}/docs/docs.csv-spec[dateAddDateTimeYears] +-------------------------------------------------- + +[source, sql] +-------------------------------------------------- +include-tagged::{sql-specs}/docs/docs.csv-spec[dateAddDateTimeWeeks] +-------------------------------------------------- + +[source, sql] +-------------------------------------------------- +include-tagged::{sql-specs}/docs/docs.csv-spec[dateAddDateTimeSeconds] +-------------------------------------------------- + +[source, sql] +-------------------------------------------------- +include-tagged::{sql-specs}/docs/docs.csv-spec[dateAddDateQuarters] +-------------------------------------------------- + +[source, sql] +-------------------------------------------------- +include-tagged::{sql-specs}/docs/docs.csv-spec[dateAddDateMinutes] +-------------------------------------------------- [[sql-functions-datetime-part]] ==== `DATE_PART/DATEPART` diff --git a/docs/reference/sql/functions/index.asciidoc b/docs/reference/sql/functions/index.asciidoc index 018787c3e5c..7b282a93e12 100644 --- a/docs/reference/sql/functions/index.asciidoc +++ b/docs/reference/sql/functions/index.asciidoc @@ -51,6 +51,7 @@ ** <> ** <> ** <> +** <> ** <> ** <> ** <> diff --git a/x-pack/plugin/sql/qa/src/main/resources/command.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/command.csv-spec index 48087f34cbf..e087ea26d3f 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/command.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/command.csv-spec @@ -41,8 +41,10 @@ CURRENT_DATE |SCALAR CURRENT_TIME |SCALAR CURRENT_TIMESTAMP|SCALAR CURTIME |SCALAR +DATEADD |SCALAR DATEPART |SCALAR DATETRUNC |SCALAR +DATE_ADD |SCALAR DATE_PART |SCALAR DATE_TRUNC |SCALAR DAY |SCALAR @@ -79,8 +81,10 @@ NOW |SCALAR QUARTER |SCALAR SECOND |SCALAR SECOND_OF_MINUTE |SCALAR +TIMESTAMPADD |SCALAR +TIMESTAMP_ADD |SCALAR TODAY |SCALAR -WEEK |SCALAR +WEEK |SCALAR WEEK_OF_YEAR |SCALAR YEAR |SCALAR ABS |SCALAR diff --git a/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec index 60b2ad52307..aa42b8b2a5c 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/datetime.csv-spec @@ -123,6 +123,160 @@ SELECT WEEK(birth_date) week, birth_date FROM test_emp WHERE WEEK(birth_date) > 2 |1953-01-07T00:00:00.000Z ; +selectAddWithDateTime +schema::dt_year:s|dt_quarter:s|dt_month:s|dt_week:s|dt_day:s|dt_hours:s|dt_min:s|dt_sec:s|dt_millis:s|dt_mcsec:s|dt_nsec:s +SELECT DATE_ADD('year', 10, '2019-09-04T11:22:33.123Z'::datetime)::string as dt_year, DATE_ADD('quarter', -10, '2019-09-04T11:22:33.123Z'::datetime)::string as dt_quarter, DATE_ADD('month', 20, '2019-09-04T11:22:33.123Z'::datetime)::string as dt_month, +DATE_ADD('week', -50, '2019-09-04T11:22:33.123Z'::datetime)::string as dt_week, DATE_ADD('day', 200, '2019-09-04T11:22:33.123Z'::datetime)::string as dt_day, +DATE_ADD('hours', -3000, '2019-09-04T11:22:33.123Z'::datetime)::string as dt_hours, DATE_ADD('minutes', 5678, '2019-09-04T11:22:33.123Z'::datetime)::string as dt_min, +DATE_ADD('second', 987654, '2019-09-04T11:22:33.123Z'::datetime)::string as dt_sec, DATE_ADD('milliseconds', -12345678, '2019-09-04T11:22:33.123Z'::datetime)::string as dt_millis, +DATE_ADD('mcs', -12345678, '2019-09-04T11:22:33.123Z'::datetime)::string as dt_mcsec, DATE_ADD('nanosecond', 12345600, '2019-09-04T11:22:33.123Z'::datetime)::string as dt_nsec; + + dt_year | dt_quarter | dt_month | dt_week | dt_day | dt_hours | dt_min | dt_sec | dt_millis | dt_mcsec | dt_nsec +-------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+------------------------- +2029-09-04T11:22:33.123Z | 2017-03-04T11:22:33.123Z | 2021-05-04T11:22:33.123Z | 2018-09-19T11:22:33.123Z | 2020-03-22T11:22:33.123Z | 2019-05-02T11:22:33.123Z | 2019-09-08T10:00:33.123Z | 2019-09-15T21:43:27.123Z | 2019-09-04T07:56:47.445Z | 2019-09-04T11:22:20.777Z | 2019-09-04T11:22:33.135Z +; + +selectAddWithDate +schema::dt_year:ts|dt_quarter:ts|dt_month:ts|dt_week:ts|dt_day:ts|dt_hours:ts|dt_min:ts|dt_sec:ts|dt_millis:s|dt_mcsec:s|dt_nsec:s +SELECT DATEADD('year', 10, '2019-09-04'::date) as dt_year, DATEADD('quarter', -10, '2019-09-04'::date) as dt_quarter, DATEADD('month', 20, '2019-09-04'::date) as dt_month, +DATEADD('week', -50, '2019-09-04'::date) as dt_week, DATEADD('day', 200, '2019-09-04'::date) as dt_day, +DATEADD('hours', -3000, '2019-09-04'::date) as dt_hours, DATEADD('minutes', 5678, '2019-09-04'::date) as dt_min, DATEADD('second', 987654, '2019-09-04'::date) as dt_sec, +DATEADD('milliseconds', 4314144, '2019-09-04'::date)::string as dt_millis, DATEADD('mcs', -12345678, '2019-09-04'::date)::string as dt_mcsec, DATEADD('nanosecond', 12345600, '2019-09-04'::date)::string as dt_nsec; + + dt_year | dt_quarter | dt_month | dt_week | dt_day | dt_hours | dt_min | dt_sec | dt_millis | dt_mcsec | dt_nsec +-------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+------------------------- +2029-09-04 00:00:00.000Z | 2017-03-04 00:00:00.000Z | 2021-05-04 00:00:00.000Z | 2018-09-19 00:00:00.000Z | 2020-03-22 00:00:00.000Z | 2019-05-02 00:00:00.000Z | 2019-09-07 22:38:00.000Z | 2019-09-15 10:20:54.000Z | 2019-09-04T01:11:54.144Z | 2019-09-03T23:59:47.654Z | 2019-09-04T00:00:00.012Z +; + +selectDateAddWithField +schema::emp_no:i|birth_date:ts|dt_year:ts|dt_quarter:ts|dt_month:ts|dt_week:ts|dt_day:ts|dt_min:ts|dt_sec:ts +SELECT emp_no, birth_date, TIMESTAMP_ADD('year', 10, birth_date) as dt_year, TIMESTAMP_ADD('quarter', -10, birth_date) as dt_quarter, +TIMESTAMP_ADD('month', 20, birth_date) as dt_month, TIMESTAMP_ADD('week', -20, birth_date) as dt_week, TIMESTAMP_ADD('day', 300, birth_date) as dt_day, +TIMESTAMP_ADD('minutes', 10000, birth_date) as dt_min, TIMESTAMP_ADD('second', 100000, birth_date) as dt_sec +FROM test_emp WHERE emp_no >= 10032 AND emp_no <= 10042 ORDER BY 1; + + emp_no | birth_date | dt_year | dt_quarter | dt_month | dt_week | dt_day | dt_min | dt_sec +----------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+--------------------------+------------------------- +10032 | 1960-08-09 00:00:00.000Z | 1970-08-09 00:00:00.000Z | 1958-02-09 00:00:00.000Z | 1962-04-09 00:00:00.000Z | 1960-03-22 00:00:00.000Z | 1961-06-05 00:00:00.000Z | 1960-08-15 22:40:00.000Z | 1960-08-10 03:46:40.000Z +10033 | 1956-11-14 00:00:00.000Z | 1966-11-14 00:00:00.000Z | 1954-05-14 00:00:00.000Z | 1958-07-14 00:00:00.000Z | 1956-06-27 00:00:00.000Z | 1957-09-10 00:00:00.000Z | 1956-11-20 22:40:00.000Z | 1956-11-15 03:46:40.000Z +10034 | 1962-12-29 00:00:00.000Z | 1972-12-29 00:00:00.000Z | 1960-06-29 00:00:00.000Z | 1964-08-29 00:00:00.000Z | 1962-08-11 00:00:00.000Z | 1963-10-25 00:00:00.000Z | 1963-01-04 22:40:00.000Z | 1962-12-30 03:46:40.000Z +10035 | 1953-02-08 00:00:00.000Z | 1963-02-08 00:00:00.000Z | 1950-08-08 00:00:00.000Z | 1954-10-08 00:00:00.000Z | 1952-09-21 00:00:00.000Z | 1953-12-05 00:00:00.000Z | 1953-02-14 22:40:00.000Z | 1953-02-09 03:46:40.000Z +10036 | 1959-08-10 00:00:00.000Z | 1969-08-10 00:00:00.000Z | 1957-02-10 00:00:00.000Z | 1961-04-10 00:00:00.000Z | 1959-03-23 00:00:00.000Z | 1960-06-05 00:00:00.000Z | 1959-08-16 22:40:00.000Z | 1959-08-11 03:46:40.000Z +10037 | 1963-07-22 00:00:00.000Z | 1973-07-22 00:00:00.000Z | 1961-01-22 00:00:00.000Z | 1965-03-22 00:00:00.000Z | 1963-03-04 00:00:00.000Z | 1964-05-17 00:00:00.000Z | 1963-07-28 22:40:00.000Z | 1963-07-23 03:46:40.000Z +10038 | 1960-07-20 00:00:00.000Z | 1970-07-20 00:00:00.000Z | 1958-01-20 00:00:00.000Z | 1962-03-20 00:00:00.000Z | 1960-03-02 00:00:00.000Z | 1961-05-16 00:00:00.000Z | 1960-07-26 22:40:00.000Z | 1960-07-21 03:46:40.000Z +10039 | 1959-10-01 00:00:00.000Z | 1969-10-01 00:00:00.000Z | 1957-04-01 00:00:00.000Z | 1961-06-01 00:00:00.000Z | 1959-05-14 00:00:00.000Z | 1960-07-27 00:00:00.000Z | 1959-10-07 22:40:00.000Z | 1959-10-02 03:46:40.000Z +10040 | null | null | null | null | null | null | null | null +10041 | null | null | null | null | null | null | null | null +10042 | null | null | null | null | null | null | null | null +; + +selectAddWithComplexExpressions1 +SELECT gender, birth_date, TIMESTAMPADD('months', CASE WHEN gender = 'M' THEN 10 WHEN gender = 'F' THEN -10 ELSE 100 END, +birth_date + INTERVAL 10 month) AS dt FROM test_emp WHERE dt > '1954-07-01'::date ORDER BY emp_no LIMIT 10; + + gender:s | birth_date:ts | dt:ts +------------+--------------------------+------------------------- +M | 1953-09-02 00:00:00.000Z | 1955-05-02 00:00:00.000Z +F | 1964-06-02 00:00:00.000Z | 1964-06-02 00:00:00.000Z +M | 1959-12-03 00:00:00.000Z | 1961-08-03 00:00:00.000Z +M | 1954-05-01 00:00:00.000Z | 1956-01-01 00:00:00.000Z +M | 1955-01-21 00:00:00.000Z | 1956-09-21 00:00:00.000Z +F | 1957-05-23 00:00:00.000Z | 1957-05-23 00:00:00.000Z +M | 1958-02-19 00:00:00.000Z | 1959-10-19 00:00:00.000Z +null | 1963-06-01 00:00:00.000Z | 1972-08-01 00:00:00.000Z +null | 1953-11-07 00:00:00.000Z | 1963-01-07 00:00:00.000Z +null | 1960-10-04 00:00:00.000Z | 1969-12-04 00:00:00.000Z +; + +selectAddWithComplexExpressions2 +schema::languages:byte|first_name:s|gender:s|hire_date:ts|date_add:date +SELECT languages, first_name, gender, hire_date, +CAST(DATE_ADD(CASE WHEN gender = 'M' THEN CONCAT(gender, 'onths') WHEN gender = 'F' THEN NULL ELSE 'quarter' END, + 5, hire_date + INTERVAL 10 month) AS DATE) AS date_add +FROM test_emp WHERE languages >= 3 AND first_name LIKE '%y%' ORDER BY date_add ASC, languages DESC; + + languages | first_name | gender | hire_date | date_add +---------------+---------------+---------------+-------------------------+--------------- +5 | Hironoby | F | 1988-07-21 00:00:00.00Z | null +4 | Weiyi | F | 1993-02-14 00:00:00.00Z | null +3 | Magy | F | 1993-03-21 00:00:00.00Z | null +4 | Jayson | M | 1990-01-14 00:00:00.00Z | 1991-04-14 +5 | Mary | null | 1990-01-22 00:00:00.00Z | 1992-02-22 +5 | Georgy | M | 1992-04-27 00:00:00.00Z | 1993-07-27 +4 | Mayumi | M | 1995-03-13 00:00:00.00Z | 1996-06-13 +; + +selectAddWithComplexExpressions3 +schema::first_name:s|gender:s|hire_date:ts|date_add1:ts|date_add2:i +SELECT first_name, gender, hire_date, DATE_ADD(CASE WHEN gender = 'M' THEN CONCAT(gender, 'onths') WHEN gender = 'F' THEN 'year' ELSE 'quarter' END, +5, hire_date + INTERVAL 10 month) AS date_add1, +YEAR(DATE_ADD(CASE WHEN gender = 'M' THEN CONCAT(gender, 'onths') WHEN gender = 'F' THEN 'year' ELSE 'quarter' END, +5, hire_date + INTERVAL 10 month)) AS date_add2 +FROM test_emp WHERE YEAR(date_add1) > 1990 AND first_name LIKE '%y%' ORDER BY date_add1 DESC LIMIT 15; + + first_name | gender | hire_date | date_add1 | date_add2 +---------------+---------------+-------------------------+-------------------------+------------ +Magy | F | 1993-03-21 00:00:00.00Z | 1999-01-21 00:00:00.00Z | 1999 +Weiyi | F | 1993-02-14 00:00:00.00Z | 1998-12-14 00:00:00.00Z | 1998 +Mayumi | M | 1995-03-13 00:00:00.00Z | 1996-06-13 00:00:00.00Z | 1996 +Saniya | M | 1994-09-15 00:00:00.00Z | 1995-12-15 00:00:00.00Z | 1995 +Hironoby | F | 1988-07-21 00:00:00.00Z | 1994-05-21 00:00:00.00Z | 1994 +Georgy | M | 1992-04-27 00:00:00.00Z | 1993-07-27 00:00:00.00Z | 1993 +Mayuko | M | 1991-01-26 00:00:00.00Z | 1992-04-26 00:00:00.00Z | 1992 +Mary | null | 1990-01-22 00:00:00.00Z | 1992-02-22 00:00:00.00Z | 1992 +Yishay | M | 1990-10-20 00:00:00.00Z | 1992-01-20 00:00:00.00Z | 1992 +Jayson | M | 1990-01-14 00:00:00.00Z | 1991-04-14 00:00:00.00Z | 1991 +; + +dateAddOrderBy +schema::emp_no:i|hire_date:ts|dt:ts +SELECT emp_no, hire_date, DATE_ADD('hours', -4000, hire_date) as dt FROM test_emp ORDER BY dt NULLS LAST, emp_no LIMIT 5; + + emp_no | hire_date | dt +--------+--------------------------+------------------------- +10009 | 1985-02-18 00:00:00.000Z | 1984-09-04 08:00:00.000Z +10048 | 1985-02-24 00:00:00.000Z | 1984-09-10 08:00:00.000Z +10098 | 1985-05-13 00:00:00.000Z | 1984-11-27 08:00:00.000Z +10076 | 1985-07-09 00:00:00.000Z | 1985-01-23 08:00:00.000Z +10061 | 1985-09-17 00:00:00.000Z | 1985-04-03 08:00:00.000Z +; + +dateAddFilter +schema::emp_no:i|hire_date:ts|dt:ts +SELECT emp_no, hire_date, DATE_ADD('quarter', 42, hire_date) as dt FROM test_emp WHERE DATE_ADD('quarter', 42, hire_date) > '2000-01-01'::date ORDER BY emp_no LIMIT 5; + + emp_no | hire_date | dt +--------+--------------------------+------------------------- +10005 | 1989-09-12 00:00:00.000Z | 2000-03-12 00:00:00.000Z +10008 | 1994-09-15 00:00:00.000Z | 2005-03-15 00:00:00.000Z +10010 | 1989-08-24 00:00:00.000Z | 2000-02-24 00:00:00.000Z +10011 | 1990-01-22 00:00:00.000Z | 2000-07-22 00:00:00.000Z +10012 | 1992-12-18 00:00:00.000Z | 2003-06-18 00:00:00.000Z +; + +dateAddGroupBy +schema::count:l|dt:ts +SELECT count(*) as count, DATE_ADD('weeks', -120, hire_date) dt FROM test_emp GROUP BY dt ORDER BY 2 LIMIT 5; + + count | dt +--------+------------------------- +1 | 1982-11-01 00:00:00.000Z +1 | 1982-11-07 00:00:00.000Z +1 | 1983-01-24 00:00:00.000Z +1 | 1983-03-22 00:00:00.000Z +1 | 1983-05-31 00:00:00.000Z +; + +dateAddHaving +schema::gender:s|dt:ts +SELECT gender, DATE_ADD('months', 60, max(hire_date)) AS dt FROM test_emp GROUP BY gender HAVING DATE_ADD('months', 60, max(hire_date)) >= '2002-01-01T00:00:00.000Z'::timestamp ORDER BY 1; + + gender | dt +--------+------------------------- +null | 2004-04-30 00:00:00.000Z +F | 2002-05-19 00:00:00.000Z +; + selectDateTruncWithDateTime schema::dt_hour:ts|dt_min:ts|dt_sec:ts|dt_millis:s|dt_micro:s|dt_nano:s SELECT DATE_TRUNC('hour', '2019-09-04T11:22:33.123Z'::datetime) as dt_hour, DATE_TRUNC('minute', '2019-09-04T11:22:33.123Z'::datetime) as dt_min, diff --git a/x-pack/plugin/sql/qa/src/main/resources/docs/docs.csv-spec b/x-pack/plugin/sql/qa/src/main/resources/docs/docs.csv-spec index b2536271b22..2999928d04e 100644 --- a/x-pack/plugin/sql/qa/src/main/resources/docs/docs.csv-spec +++ b/x-pack/plugin/sql/qa/src/main/resources/docs/docs.csv-spec @@ -237,8 +237,10 @@ CURRENT_DATE |SCALAR CURRENT_TIME |SCALAR CURRENT_TIMESTAMP|SCALAR CURTIME |SCALAR +DATEADD |SCALAR DATEPART |SCALAR DATETRUNC |SCALAR +DATE_ADD |SCALAR DATE_PART |SCALAR DATE_TRUNC |SCALAR DAY |SCALAR @@ -275,8 +277,10 @@ NOW |SCALAR QUARTER |SCALAR SECOND |SCALAR SECOND_OF_MINUTE |SCALAR +TIMESTAMPADD |SCALAR +TIMESTAMP_ADD |SCALAR TODAY |SCALAR -WEEK |SCALAR +WEEK |SCALAR WEEK_OF_YEAR |SCALAR YEAR |SCALAR ABS |SCALAR @@ -2417,6 +2421,56 @@ SELECT DAY_OF_MONTH(CAST('2018-02-19T10:23:27Z' AS TIMESTAMP)) AS day; // end::dayOfMonth ; +dateAddDateTimeYears +// tag::dateAddDateTimeYears +SELECT DATE_ADD('years', 10, '2019-09-04T11:22:33.000Z'::datetime) AS "+10 years"; + + +10 years +------------------------ +2029-09-04T11:22:33.000Z +// end::dateAddDateTimeYears +; + +dateAddDateTimeWeeks +// tag::dateAddDateTimeWeeks +SELECT DATE_ADD('week', 10, '2019-09-04T11:22:33.000Z'::datetime) AS "+10 weeks"; + + +10 weeks +------------------------ +2019-11-13T11:22:33.000Z +// end::dateAddDateTimeWeeks +; + +dateAddDateTimeSeconds +// tag::dateAddDateTimeSeconds +SELECT DATE_ADD('seconds', -1234, '2019-09-04T11:22:33.000Z'::datetime) AS "-1234 seconds"; + + -1234 seconds +------------------------ +2019-09-04T11:01:59.000Z +// end::dateAddDateTimeSeconds +; + +dateAddDateQuarters +// tag::dateAddDateQuarters +SELECT DATE_ADD('qq', -417, '2019-09-04'::date) AS "-417 quarters"; + + -417 quarters +------------------------ +1915-06-04T00:00:00.000Z +// end::dateAddDateQuarters +; + +dateAddDateMinutes +// tag::dateAddDateMinutes +SELECT DATE_ADD('minutes', 9235, '2019-09-04'::date) AS "+9235 minutes"; + + +9235 minutes +------------------------ +2019-09-10T09:55:00.000Z +// end::dateAddDateMinutes +; + datePartDateTimeYears // tag::datePartDateTimeYears SELECT DATE_PART('year', '2019-09-22T11:22:33.123Z'::datetime) AS "years"; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java index ee6bb1d30e1..811bc4c76b7 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java @@ -31,6 +31,7 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.User; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.CurrentDate; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.CurrentDateTime; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.CurrentTime; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateAdd; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DatePart; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTrunc; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DayName; @@ -195,6 +196,7 @@ public class FunctionRegistry { def(DayOfMonth.class, DayOfMonth::new, "DAY_OF_MONTH", "DAYOFMONTH", "DAY", "DOM"), def(DayOfWeek.class, DayOfWeek::new, "DAY_OF_WEEK", "DAYOFWEEK", "DOW"), def(DayOfYear.class, DayOfYear::new, "DAY_OF_YEAR", "DAYOFYEAR", "DOY"), + def(DateAdd.class, DateAdd::new, "DATEADD", "DATE_ADD", "TIMESTAMPADD", "TIMESTAMP_ADD"), def(DatePart.class, DatePart::new, "DATEPART", "DATE_PART"), def(DateTrunc.class, DateTrunc::new, "DATETRUNC", "DATE_TRUNC"), def(HourOfDay.class, HourOfDay::new, "HOUR_OF_DAY", "HOUR"), @@ -377,7 +379,7 @@ public class FunctionRegistry { }; return def(function, builder, false, names); } - + interface ConfigurationAwareFunctionBuilder { T build(Source source, Configuration configuration); } @@ -446,7 +448,7 @@ public class FunctionRegistry { interface MultiFunctionBuilder { T build(Source source, List children); } - + /** * Build a {@linkplain FunctionDefinition} for a unary function that is not * aware of time zone but does support {@code DISTINCT}. @@ -512,6 +514,28 @@ public class FunctionRegistry { T build(Source source, Expression lhs, Expression rhs, ZoneId zi); } + /** + * Build a {@linkplain FunctionDefinition} for a three-args function that + * requires a timezone. + */ + @SuppressWarnings("overloads") // These are ambiguous if you aren't using ctor references but we always do + static FunctionDefinition def(Class function, DatetimeThreeArgsFunctionBuilder ctorRef, String... names) { + FunctionBuilder builder = (source, children, distinct, cfg) -> { + if (children.size() != 3) { + throw new SqlIllegalArgumentException("expects three arguments"); + } + if (distinct) { + throw new SqlIllegalArgumentException("does not support DISTINCT yet it was specified"); + } + return ctorRef.build(source, children.get(0), children.get(1), children.get(2), cfg.zoneId()); + }; + return def(function, builder, false, names); + } + + interface DatetimeThreeArgsFunctionBuilder { + T build(Source source, Expression first, Expression second, Expression third, ZoneId zi); + } + /** * Build a {@linkplain FunctionDefinition} for a binary function that is * not aware of time zone and does not support {@code DISTINCT}. diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java index ec0f0cd0345..c42838b78f8 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/Processors.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.sql.expression.function.scalar; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.NamedWriteableRegistry.Entry; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateAddProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DatePartProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTruncProcessor; @@ -90,6 +91,7 @@ public final class Processors { entries.add(new Entry(Processor.class, NamedDateTimeProcessor.NAME, NamedDateTimeProcessor::new)); entries.add(new Entry(Processor.class, NonIsoDateTimeProcessor.NAME, NonIsoDateTimeProcessor::new)); entries.add(new Entry(Processor.class, QuarterProcessor.NAME, QuarterProcessor::new)); + entries.add(new Entry(Processor.class, DateAddProcessor.NAME, DateAddProcessor::new)); entries.add(new Entry(Processor.class, DatePartProcessor.NAME, DatePartProcessor::new)); entries.add(new Entry(Processor.class, DateTruncProcessor.NAME, DateTruncProcessor::new)); // math diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAdd.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAdd.java new file mode 100644 index 00000000000..b7d3c522d9d --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAdd.java @@ -0,0 +1,173 @@ +/* + * 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.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.Expressions; +import org.elasticsearch.xpack.sql.expression.Nullability; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; +import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.tree.Source; +import org.elasticsearch.xpack.sql.type.DataType; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; + +import static org.elasticsearch.common.logging.LoggerMessageFormat.format; +import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isDate; +import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isInteger; +import static org.elasticsearch.xpack.sql.expression.TypeResolutions.isString; + +public class DateAdd extends ThreeArgsDateTimeFunction { + + public enum Part implements DateTimeField { + YEAR((dt, i) -> dt.plus(i, ChronoUnit.YEARS), "years", "yyyy", "yy"), + QUARTER((dt, i) -> dt.plus(i * 3, ChronoUnit.MONTHS), "quarters", "qq", "q"), + MONTH((dt, i) -> dt.plus(i, ChronoUnit.MONTHS), "months", "mm", "m"), + DAYOFYEAR((dt, i) -> dt.plus(i, ChronoUnit.DAYS), "dy", "y"), + DAY((dt, i) -> dt.plus(i, ChronoUnit.DAYS), "days", "dd", "d"), + WEEK((dt, i) -> dt.plus(i, ChronoUnit.WEEKS), "weeks", "wk", "ww"), + WEEKDAY((dt, i) -> dt.plus(i, ChronoUnit.DAYS), "weekdays", "dw"), + HOUR((dt, i) -> dt.plus(i, ChronoUnit.HOURS), "hours", "hh"), + MINUTE((dt, i) -> dt.plus(i, ChronoUnit.MINUTES), "minutes", "mi", "n"), + SECOND((dt, i) -> dt.plus(i, ChronoUnit.SECONDS), "seconds", "ss", "s"), + MILLISECOND((dt, i) -> dt.plus(i, ChronoUnit.MILLIS), "milliseconds", "ms"), + MICROSECOND((dt, i) -> dt.plus(i, ChronoUnit.MICROS), "microseconds", "mcs"), + NANOSECOND((dt, i) -> dt.plus(i, ChronoUnit.NANOS), "nanoseconds", "ns"); + + private static final Map NAME_TO_PART; + private static final List VALID_VALUES; + + static { + NAME_TO_PART = DateTimeField.initializeResolutionMap(values()); + VALID_VALUES = DateTimeField.initializeValidValues(values()); + } + + private BiFunction addFunction; + private Set aliases; + + Part(BiFunction addFunction, String... aliases) { + this.addFunction = addFunction; + this.aliases = new HashSet<>(Arrays.asList(aliases)); + } + + @Override + public Iterable aliases() { + return aliases; + } + + public static List findSimilar(String match) { + return DateTimeField.findSimilar(NAME_TO_PART.keySet(), match); + } + + public static Part resolve(String truncateTo) { + return DateTimeField.resolveMatch(NAME_TO_PART, truncateTo); + } + + public ZonedDateTime add(ZonedDateTime dateTime, Integer numberOfUnits) { + return addFunction.apply(dateTime, numberOfUnits); + } + } + + public DateAdd(Source source, Expression unit, Expression numberOfUnits, Expression timestamp, ZoneId zoneId) { + super(source, unit, numberOfUnits, timestamp, zoneId); + } + + @Override + protected TypeResolution resolveType() { + TypeResolution resolution = isString(first(), sourceText(), Expressions.ParamOrdinal.FIRST); + if (resolution.unresolved()) { + return resolution; + } + + if (first().foldable()) { + String datePartValue = (String) first().fold(); + if (datePartValue != null && resolveDateTimeField(datePartValue) == false) { + List similar = findSimilarDateTimeFields(datePartValue); + if (similar.isEmpty()) { + return new TypeResolution(format(null, "first argument of [{}] must be one of {} or their aliases; found value [{}]", + sourceText(), + validDateTimeFieldValues(), + Expressions.name(first()))); + } else { + return new TypeResolution(format(null, "Unknown value [{}] for first argument of [{}]; did you mean {}?", + Expressions.name(first()), + sourceText(), + similar)); + } + } + } + + resolution = isInteger(second(), sourceText(), Expressions.ParamOrdinal.SECOND); + if (resolution.unresolved()) { + return resolution; + } + + resolution = isDate(third(), sourceText(), Expressions.ParamOrdinal.THIRD); + if (resolution.unresolved()) { + return resolution; + } + + return TypeResolution.TYPE_RESOLVED; + } + + @Override + public DataType dataType() { + return DataType.DATETIME; + } + + @Override + protected ThreeArgsDateTimeFunction replaceChildren(Expression newFirst, Expression newSecond, Expression newThird) { + return new DateAdd(source(), newFirst, newSecond, newThird, zoneId()); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, DateAdd::new, first(), second(), third(), zoneId()); + } + + @Override + public Nullability nullable() { + return Nullability.UNKNOWN; + } + + @Override + protected Pipe createPipe(Pipe first, Pipe second, Pipe third, ZoneId zoneId) { + return new DateAddPipe(source(), this, first, second, third, zoneId); + } + + @Override + protected String scriptMethodName() { + return "dateAdd"; + } + + @Override + public Object fold() { + return DateAddProcessor.process(first().fold(), second().fold(), third().fold(), zoneId()); + } + + @Override + protected boolean resolveDateTimeField(String dateTimeField) { + return Part.resolve(dateTimeField) != null; + } + + @Override + protected List findSimilarDateTimeFields(String dateTimeField) { + return Part.findSimilar(dateTimeField); + } + + @Override + protected List validDateTimeFieldValues() { + return Part.VALID_VALUES; + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddPipe.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddPipe.java new file mode 100644 index 00000000000..6dbda129ecd --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddPipe.java @@ -0,0 +1,36 @@ +/* + * 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.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; +import org.elasticsearch.xpack.sql.expression.gen.processor.Processor; +import org.elasticsearch.xpack.sql.tree.NodeInfo; +import org.elasticsearch.xpack.sql.tree.Source; + +import java.time.ZoneId; + +public class DateAddPipe extends ThreeArgsDateTimePipe { + + public DateAddPipe(Source source, Expression expression, Pipe first, Pipe second, Pipe third, ZoneId zoneId) { + super(source, expression, first, second, third, zoneId); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, DateAddPipe::new, expression(), first(), second(), third(), zoneId()); + } + + @Override + public ThreeArgsDateTimePipe replaceChildren(Pipe newFirst, Pipe newSecond, Pipe newThird) { + return new DateAddPipe(source(), expression(), newFirst, newSecond, newThird, zoneId()); + } + + @Override + protected Processor makeProcessor(Processor first, Processor second, Processor third, ZoneId zoneId) { + return new DateAddProcessor(first, second, third, zoneId); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddProcessor.java new file mode 100644 index 00000000000..434e17a472f --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddProcessor.java @@ -0,0 +1,72 @@ +/* + * 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.common.io.stream.StreamInput; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateAdd.Part; +import org.elasticsearch.xpack.sql.expression.gen.processor.Processor; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; + +public class DateAddProcessor extends ThreeArgsDateTimeProcessor { + + public static final String NAME = "dtadd"; + + public DateAddProcessor(Processor unit, Processor numberOfUnits, Processor timestamp, ZoneId zoneId) { + super(unit, numberOfUnits, timestamp, zoneId); + } + + public DateAddProcessor(StreamInput in) throws IOException { + super(in); + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public Object doProcess(Object unit, Object numberOfUnits, Object timestamp, ZoneId zoneId) { + return process(unit, numberOfUnits, timestamp, zoneId); + } + + /** + * Used in Painless scripting + */ + public static Object process(Object unit, Object numberOfUnits, Object timestamp, ZoneId zoneId) { + if (unit == null || numberOfUnits == null || timestamp == null) { + return null; + } + if (unit instanceof String == false) { + throw new SqlIllegalArgumentException("A string is required; received [{}]", unit); + } + Part datePartField = Part.resolve((String) unit); + if (datePartField == null) { + List similar = Part.findSimilar((String) unit); + if (similar.isEmpty()) { + throw new SqlIllegalArgumentException("A value of {} or their aliases is required; received [{}]", + Part.values(), unit); + } else { + throw new SqlIllegalArgumentException("Received value [{}] is not valid date part to add; " + + "did you mean {}?", unit, similar); + } + } + + if (numberOfUnits instanceof Integer == false) { + throw new SqlIllegalArgumentException("An integer is required; received [{}]", numberOfUnits); + } + + if (timestamp instanceof ZonedDateTime == false) { + throw new SqlIllegalArgumentException("A date/datetime is required; received [{}]", timestamp); + } + + return datePartField.add(((ZonedDateTime) timestamp).withZoneSameInstant(zoneId), (Integer) numberOfUnits); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimeFunction.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimeFunction.java new file mode 100644 index 00000000000..1be010f32d3 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimeFunction.java @@ -0,0 +1,127 @@ +/* + * 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.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.Expressions; +import org.elasticsearch.xpack.sql.expression.Nullability; +import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; +import org.elasticsearch.xpack.sql.expression.gen.script.ScriptTemplate; +import org.elasticsearch.xpack.sql.tree.Source; + +import java.time.ZoneId; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +import static org.elasticsearch.xpack.sql.expression.gen.script.ParamsBuilder.paramsBuilder; + +public abstract class ThreeArgsDateTimeFunction extends ScalarFunction { + + private final ZoneId zoneId; + + public ThreeArgsDateTimeFunction(Source source, Expression first, Expression second, Expression third, ZoneId zoneId) { + super(source, Arrays.asList(first, second, third)); + this.zoneId = zoneId; + } + + public Expression first() { + return arguments().get(0); + } + + public Expression second() { + return arguments().get(1); + } + + public Expression third() { + return arguments().get(2); + } + + public ZoneId zoneId() { + return zoneId; + } + + protected abstract boolean resolveDateTimeField(String dateTimeField); + + protected abstract List findSimilarDateTimeFields(String dateTimeField); + + protected abstract List validDateTimeFieldValues(); + + @Override + public final ThreeArgsDateTimeFunction replaceChildren(List newChildren) { + if (newChildren.size() != 3) { + throw new IllegalArgumentException("expected [3] children but received [" + newChildren.size() + "]"); + } + return replaceChildren(newChildren.get(0), newChildren.get(1), newChildren.get(2)); + } + + protected abstract ThreeArgsDateTimeFunction replaceChildren(Expression newFirst, Expression newSecond, Expression newThird); + + @Override + protected Pipe makePipe() { + return createPipe(Expressions.pipe(first()), Expressions.pipe(second()), Expressions.pipe(third()), zoneId); + } + + protected abstract Pipe createPipe(Pipe first, Pipe second, Pipe third, ZoneId zoneId); + + @Override + public Nullability nullable() { + return Nullability.TRUE; + } + + @Override + public boolean foldable() { + return first().foldable() && second().foldable() && third().foldable(); + } + + @Override + public ScriptTemplate asScript() { + ScriptTemplate firstScript = asScript(first()); + ScriptTemplate secondScript = asScript(second()); + ScriptTemplate thirdScript = asScript(third()); + + return asScriptFrom(firstScript, secondScript, thirdScript); + } + + protected ScriptTemplate asScriptFrom(ScriptTemplate firstScript, ScriptTemplate secondScript, ScriptTemplate thirdScript) { + return new ScriptTemplate( + formatTemplate("{sql}." + scriptMethodName() + + "(" + firstScript.template() + "," + secondScript.template() + "," + thirdScript.template() + ",{})"), + paramsBuilder() + .script(firstScript.params()) + .script(secondScript.params()) + .script(thirdScript.params()) + .variable(zoneId.getId()) + .build(), + dataType()); + } + + protected String scriptMethodName() { + return getClass().getSimpleName().toLowerCase(Locale.ROOT); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), zoneId); + } + + @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; + } + ThreeArgsDateTimeFunction that = (ThreeArgsDateTimeFunction) o; + return zoneId.equals(that.zoneId); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimePipe.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimePipe.java new file mode 100644 index 00000000000..e1d60b1232b --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimePipe.java @@ -0,0 +1,86 @@ +/* + * 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.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; +import org.elasticsearch.xpack.sql.expression.gen.processor.Processor; +import org.elasticsearch.xpack.sql.tree.Source; + +import java.time.ZoneId; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public abstract class ThreeArgsDateTimePipe extends Pipe { + + private final Pipe first, second, third; + private final ZoneId zoneId; + + public ThreeArgsDateTimePipe(Source source, Expression expression, Pipe first, Pipe second, Pipe third, ZoneId zoneId) { + super(source, expression, Arrays.asList(first, second, third)); + this.first = first; + this.second = second; + this.third = third; + this.zoneId = zoneId; + } + + public Pipe first() { + return first; + } + + public Pipe second() { + return second; + } + + public Pipe third() { + return third; + } + + ZoneId zoneId() { + return zoneId; + } + + @Override + public final Pipe replaceChildren(List newChildren) { + if (newChildren.size() != 3) { + throw new IllegalArgumentException("expected [3] children but received [" + newChildren.size() + "]"); + } + return replaceChildren(newChildren.get(0), newChildren.get(1), newChildren.get(2)); + } + + public abstract Pipe replaceChildren(Pipe newFirst, Pipe newSecond, Pipe newThird); + + @Override + public Processor asProcessor() { + return makeProcessor(first.asProcessor(), second.asProcessor(), third.asProcessor(), zoneId); + } + + protected abstract Processor makeProcessor(Processor first, Processor second, Processor third, ZoneId zoneId); + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), first, second, third, zoneId); + } + + @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; + } + ThreeArgsDateTimePipe that = (ThreeArgsDateTimePipe) o; + return first.equals(that.first) && + second.equals(that.second) && + third.equals(that.third) && + zoneId.equals(that.zoneId); + } +} diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimeProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimeProcessor.java new file mode 100644 index 00000000000..bc22c83c325 --- /dev/null +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/ThreeArgsDateTimeProcessor.java @@ -0,0 +1,98 @@ +/* + * 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.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.sql.common.io.SqlStreamInput; +import org.elasticsearch.xpack.sql.expression.gen.processor.Processor; + +import java.io.IOException; +import java.time.ZoneId; +import java.util.Objects; + +public abstract class ThreeArgsDateTimeProcessor implements Processor { + + private final Processor first, second, third; + private final ZoneId zoneId; + + public ThreeArgsDateTimeProcessor(Processor first, Processor second, Processor third, ZoneId zoneId) { + this.first = first; + this.second = second; + this.third = third; + this.zoneId = zoneId; + } + + protected ThreeArgsDateTimeProcessor(StreamInput in) throws IOException { + this.first = in.readNamedWriteable(Processor.class); + this.second = in.readNamedWriteable(Processor.class); + this.third = in.readNamedWriteable(Processor.class); + zoneId = SqlStreamInput.asSqlStream(in).zoneId(); + } + + @Override + public final void writeTo(StreamOutput out) throws IOException { + out.writeNamedWriteable(first); + out.writeNamedWriteable(second); + out.writeNamedWriteable(third); + } + + public Processor first() { + return first; + } + + public Processor second() { + return second; + } + + public Processor third() { + return third; + } + + ZoneId zoneId() { + return zoneId; + } + + @Override + public Object process(Object input) { + Object o1 = first().process(input); + if (o1 == null) { + return null; + } + Object o2 = second().process(input); + if (o2 == null) { + return null; + } + Object o3 = third().process(input); + if (o3 == null) { + return null; + } + + return doProcess(o1, o2, o3, zoneId()); + } + + public abstract Object doProcess(Object o1, Object o2, Object o3, ZoneId zoneId); + + @Override + public int hashCode() { + return Objects.hash(first, second, third, zoneId); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ThreeArgsDateTimeProcessor that = (ThreeArgsDateTimeProcessor) o; + return Objects.equals(first, that.first) && + Objects.equals(second, that.second) && + Objects.equals(third, that.third) && + Objects.equals(zoneId, that.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 285c09bb4eb..c9e9bc46493 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 @@ -9,6 +9,7 @@ import org.elasticsearch.common.geo.GeoPoint; import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.script.JodaCompatibleZonedDateTime; 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.DatePartProcessor; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeFunction; import org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTruncProcessor; @@ -235,7 +236,7 @@ public final class InternalSqlScriptUtils { public static Double atan(Number value) { return MathOperation.ATAN.apply(value); } - + public static Number atan2(Number left, Number right) { return BinaryMathOperation.ATAN2.apply(left, right); } @@ -291,7 +292,7 @@ public final class InternalSqlScriptUtils { public static Double pi(Number value) { return MathOperation.PI.apply(value); } - + public static Number power(Number left, Number right) { return BinaryMathOperation.POWER.apply(left, right); } @@ -350,7 +351,7 @@ public final class InternalSqlScriptUtils { } return NonIsoDateTimeExtractor.DAY_OF_WEEK.extract(asDateTime(dateTime), tzId); } - + public static String monthName(Object dateTime, String tzId) { if (dateTime == null || tzId == null) { return null; @@ -364,7 +365,7 @@ public final class InternalSqlScriptUtils { } return QuarterProcessor.quarter(asDateTime(dateTime), tzId); } - + public static Integer weekOfYear(Object dateTime, String tzId) { if (dateTime == null || tzId == null) { return null; @@ -372,6 +373,10 @@ public final class InternalSqlScriptUtils { return NonIsoDateTimeExtractor.WEEK_OF_YEAR.extract(asDateTime(dateTime), tzId); } + public static ZonedDateTime dateAdd(String dateField, Integer numberOfUnits, Object dateTime, String tzId) { + return (ZonedDateTime) DateAddProcessor.process(dateField, numberOfUnits, asDateTime(dateTime) , ZoneId.of(tzId)); + } + public static ZonedDateTime dateTrunc(String truncateTo, Object dateTime, String tzId) { return (ZonedDateTime) DateTruncProcessor.process(truncateTo, asDateTime(dateTime) , ZoneId.of(tzId)); } 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 3d1dbc05058..0b690e26441 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 @@ -115,6 +115,7 @@ class org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalS String monthName(Object, String) Integer quarter(Object, String) Integer weekOfYear(Object, String) + ZonedDateTime dateAdd(String, Integer, Object, String) ZonedDateTime dateTrunc(String, Object, String) Integer datePart(String, Object, 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 8dde63539f5..47aae94a6a8 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 @@ -256,6 +256,34 @@ public class VerifierErrorMessagesTests extends ESTestCase { accept("SELECT DATE_PART('ms', date) FROM test"); } + public void testDateAddInvalidArgs() { + assertEquals("1:8: first argument of [DATE_ADD(int, int, date)] must be [string], found value [int] type [integer]", + error("SELECT DATE_ADD(int, int, date) FROM test")); + assertEquals("1:8: second argument of [DATE_ADD(keyword, keyword, date)] must be [integer], found value [keyword] " + + "type [keyword]", error("SELECT DATE_ADD(keyword, keyword, date) FROM test")); + assertEquals("1:8: third argument of [DATE_ADD(keyword, int, keyword)] must be [date or datetime], found value [keyword] " + + "type [keyword]", error("SELECT DATE_ADD(keyword, int, keyword) FROM test")); + assertEquals("1:8: first argument of [DATE_ADD('invalid', int, date)] must be one of [YEAR, QUARTER, MONTH, DAYOFYEAR, " + + "DAY, WEEK, WEEKDAY, HOUR, MINUTE, SECOND, MILLISECOND, MICROSECOND, NANOSECOND] " + + "or their aliases; found value ['invalid']", + error("SELECT DATE_ADD('invalid', int, date) FROM test")); + assertEquals("1:8: Unknown value ['sacinds'] for first argument of [DATE_ADD('sacinds', int, date)]; " + + "did you mean [seconds, second]?", + error("SELECT DATE_ADD('sacinds', int, date) FROM test")); + assertEquals("1:8: Unknown value ['dz'] for first argument of [DATE_ADD('dz', int, date)]; " + + "did you mean [dd, dw, dy, d]?", + error("SELECT DATE_ADD('dz', int, date) FROM test")); + } + + public void testDateAddValidArgs() { + accept("SELECT DATE_ADD('weekday', 0, date) FROM test"); + accept("SELECT DATE_ADD('dw', 20, date) FROM test"); + accept("SELECT DATE_ADD('years', -10, date) FROM test"); + accept("SELECT DATE_ADD('dayofyear', 123, date) FROM test"); + accept("SELECT DATE_ADD('dy', 30, date) FROM test"); + accept("SELECT DATE_ADD('ms', 1, date::date) 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/DateAddPipeTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddPipeTests.java new file mode 100644 index 00000000000..6e2d2ca62db --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddPipeTests.java @@ -0,0 +1,152 @@ +/* + * 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.test.ESTestCase; +import org.elasticsearch.xpack.sql.expression.Expression; +import org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils; +import org.elasticsearch.xpack.sql.expression.gen.pipeline.Pipe; +import org.elasticsearch.xpack.sql.tree.AbstractNodeTestCase; +import org.elasticsearch.xpack.sql.tree.Source; +import org.elasticsearch.xpack.sql.tree.SourceTests; + +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import static org.elasticsearch.xpack.sql.expression.Expressions.pipe; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomDatetimeLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomIntLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomStringLiteral; +import static org.elasticsearch.xpack.sql.tree.SourceTests.randomSource; + +public class DateAddPipeTests extends AbstractNodeTestCase { + + @Override + protected DateAddPipe randomInstance() { + return randomDateAddPipe(); + } + + private Expression randomDateAddPipeExpression() { + return randomDateAddPipe().expression(); + } + + public static DateAddPipe randomDateAddPipe() { + return (DateAddPipe) new DateAdd( + randomSource(), + randomStringLiteral(), + randomIntLiteral(), + randomDatetimeLiteral(), + randomZone()) + .makePipe(); + } + + @Override + public void testTransform() { + // test transforming only the properties (source, expression), + // skipping the children (the three parameters of the function) which are tested separately + DateAddPipe b1 = randomInstance(); + + Expression newExpression = randomValueOtherThan(b1.expression(), this::randomDateAddPipeExpression); + DateAddPipe newB = new DateAddPipe( + b1.source(), + newExpression, + b1.first(), + b1.second(), + b1.third(), + b1.zoneId()); + assertEquals(newB, b1.transformPropertiesOnly(v -> Objects.equals(v, b1.expression()) ? newExpression : v, Expression.class)); + + DateAddPipe b2 = randomInstance(); + Source newLoc = randomValueOtherThan(b2.source(), SourceTests::randomSource); + newB = new DateAddPipe( + newLoc, + b2.expression(), + b2.first(), + b2.second(), + b2.third(), + b2.zoneId()); + assertEquals(newB, + b2.transformPropertiesOnly(v -> Objects.equals(v, b2.source()) ? newLoc : v, Source.class)); + } + + @Override + public void testReplaceChildren() { + DateAddPipe b = randomInstance(); + Pipe newFirst = pipe(((Expression) randomValueOtherThan(b.first(), FunctionTestUtils::randomStringLiteral))); + Pipe newSecond = pipe(((Expression) randomValueOtherThan(b.second(), FunctionTestUtils::randomIntLiteral))); + Pipe newThird = pipe(((Expression) randomValueOtherThan(b.third(), FunctionTestUtils::randomDatetimeLiteral))); + ZoneId newZoneId = randomValueOtherThan(b.zoneId(), ESTestCase::randomZone); + DateAddPipe newB = new DateAddPipe( b.source(), b.expression(), b.first(), b.second(), b.third(), newZoneId); + + ThreeArgsDateTimePipe transformed = newB.replaceChildren(newFirst, b.second(), b.third()); + assertEquals(transformed.source(), b.source()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.first(), newFirst); + assertEquals(transformed.second(), b.second()); + + transformed = newB.replaceChildren(b.first(), newSecond, b.third()); + assertEquals(transformed.source(), b.source()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.first(), b.first()); + assertEquals(transformed.second(), newSecond); + + transformed = newB.replaceChildren(b.first(), b.second(), newThird); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.source(), b.source()); + assertEquals(transformed.first(), b.first()); + assertEquals(transformed.second(), b.second()); + assertEquals(transformed.third(), newThird); + + transformed = newB.replaceChildren(newFirst, newSecond, newThird); + assertEquals(transformed.source(), b.source()); + assertEquals(transformed.expression(), b.expression()); + assertEquals(transformed.first(), newFirst); + assertEquals(transformed.second(), newSecond); + assertEquals(transformed.third(), newThird); + } + + @Override + protected DateAddPipe mutate(DateAddPipe instance) { + List> randoms = new ArrayList<>(); + randoms.add(f -> new DateAddPipe(f.source(), f.expression(), + pipe(((Expression) randomValueOtherThan(f.first(), FunctionTestUtils::randomStringLiteral))), + f.second(), + f.third(), + randomValueOtherThan(f.zoneId(), ESTestCase::randomZone))); + randoms.add(f -> new DateAddPipe(f.source(), f.expression(), + f.first(), + pipe(((Expression) randomValueOtherThan(f.second(), FunctionTestUtils::randomIntLiteral))), + f.third(), + randomValueOtherThan(f.zoneId(), ESTestCase::randomZone))); + randoms.add(f -> new DateAddPipe(f.source(), f.expression(), + f.first(), + f.second(), + pipe(((Expression) randomValueOtherThan(f.third(), FunctionTestUtils::randomDatetimeLiteral))), + randomValueOtherThan(f.zoneId(), ESTestCase::randomZone))); + randoms.add(f -> new DateAddPipe(f.source(), f.expression(), + pipe(((Expression) randomValueOtherThan(f.first(), FunctionTestUtils::randomStringLiteral))), + pipe(((Expression) randomValueOtherThan(f.second(), FunctionTestUtils::randomIntLiteral))), + pipe(((Expression) randomValueOtherThan(f.third(), FunctionTestUtils::randomDatetimeLiteral))), + randomValueOtherThan(f.zoneId(), ESTestCase::randomZone))); + + return randomFrom(randoms).apply(instance); + } + + @Override + protected DateAddPipe copy(DateAddPipe instance) { + return new DateAddPipe( + instance.source(), + instance.expression(), + instance.first(), + instance.second(), + instance.third(), + instance.zoneId()); + } +} diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddProcessorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddProcessorTests.java new file mode 100644 index 00000000000..84dd04027c5 --- /dev/null +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateAddProcessorTests.java @@ -0,0 +1,201 @@ +/* + * 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.common.io.stream.Writeable.Reader; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.AbstractSqlWireSerializingTestCase; +import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.expression.Literal; +import org.elasticsearch.xpack.sql.expression.gen.processor.ConstantProcessor; +import org.elasticsearch.xpack.sql.tree.Source; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.elasticsearch.xpack.sql.expression.Literal.NULL; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.l; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomDatetimeLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.FunctionTestUtils.randomIntLiteral; +import static org.elasticsearch.xpack.sql.expression.function.scalar.datetime.DateTimeTestUtils.dateTime; +import static org.elasticsearch.xpack.sql.proto.StringUtils.ISO_DATE_WITH_NANOS; + +public class DateAddProcessorTests extends AbstractSqlWireSerializingTestCase { + + public static DateAddProcessor randomDateAddProcessor() { + return new DateAddProcessor( + new ConstantProcessor(randomRealisticUnicodeOfLengthBetween(0, 128)), + new ConstantProcessor(randomInt()), + new ConstantProcessor(DateTimeTestUtils.nowWithMillisResolution()), + randomZone()); + } + + @Override + protected DateAddProcessor createTestInstance() { + return randomDateAddProcessor(); + } + + @Override + protected Reader instanceReader() { + return DateAddProcessor::new; + } + + @Override + protected ZoneId instanceZoneId(DateAddProcessor instance) { + return instance.zoneId(); + } + + @Override + protected DateAddProcessor mutateInstance(DateAddProcessor instance) { + return new DateAddProcessor( + new ConstantProcessor(ESTestCase.randomRealisticUnicodeOfLength(128)), + new ConstantProcessor(randomValueOtherThan((Integer) instance.second().process(null), ESTestCase::randomInt)), + new ConstantProcessor(DateTimeTestUtils.nowWithMillisResolution()), + randomValueOtherThan(instance.zoneId(), ESTestCase::randomZone)); + } + + public void testInvalidInputs() { + SqlIllegalArgumentException siae = expectThrows(SqlIllegalArgumentException.class, + () -> new DateAdd(Source.EMPTY, + l(5), l(10), randomDatetimeLiteral(), randomZone()).makePipe().asProcessor().process(null)); + assertEquals("A string is required; received [5]", siae.getMessage()); + + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new DateAdd(Source.EMPTY, + l("days"), l("foo"), randomDatetimeLiteral(), randomZone()).makePipe().asProcessor().process(null)); + assertEquals("An integer is required; received [foo]", siae.getMessage()); + + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new DateAdd(Source.EMPTY, + l("days"), l(10), l("foo"), randomZone()).makePipe().asProcessor().process(null)); + assertEquals("A date/datetime is required; received [foo]", siae.getMessage()); + + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new DateAdd(Source.EMPTY, + l("invalid"), l(10), randomDatetimeLiteral(), randomZone()).makePipe().asProcessor().process(null)); + assertEquals("A value of [YEAR, QUARTER, MONTH, DAYOFYEAR, DAY, WEEK, WEEKDAY, HOUR, MINUTE, " + + "SECOND, MILLISECOND, MICROSECOND, NANOSECOND] or their aliases is required; received [invalid]", + siae.getMessage()); + + siae = expectThrows(SqlIllegalArgumentException.class, + () -> new DateAdd(Source.EMPTY, + l("quertar"), l(10), randomDatetimeLiteral(), randomZone()).makePipe().asProcessor().process(null)); + assertEquals("Received value [quertar] is not valid date part to add; did you mean [quarter, quarters]?", + siae.getMessage()); + } + + public void testWithNulls() { + assertNull(new DateAdd(Source.EMPTY, + NULL, randomIntLiteral(), randomDatetimeLiteral(), randomZone()).makePipe().asProcessor().process(null)); + assertNull(new DateAdd(Source.EMPTY, + l("days"), NULL, randomDatetimeLiteral(), randomZone()).makePipe().asProcessor().process(null)); + assertNull(new DateAdd(Source.EMPTY, + l("days"), randomIntLiteral(), NULL, randomZone()).makePipe().asProcessor().process(null)); + assertNull(new DateAdd(Source.EMPTY, + NULL, NULL, NULL, randomZone()).makePipe().asProcessor().process(null)); + } + + public void testAddition() { + ZoneId zoneId = ZoneId.of("Etc/GMT-10"); + Literal dateTime = l(dateTime(2019, 9, 3, 18, 10, 37, 123456789)); + + assertEquals("2029-09-04T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("years"), l(10), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2009-09-04T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("years"), l(-10), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2022-03-04T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("quarters"), l(10), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2017-03-04T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("quarters"), l(-10), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2021-05-04T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("month"), l(20), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2018-01-04T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("month"), l(-20), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2020-05-01T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("day"), l(240), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2019-05-07T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("day"), l(-120), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2020-12-25T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("dayofyear"), l(478), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2018-05-14T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("dayofyear"), l(-478), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2021-12-22T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("weeks"), l(120), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2017-05-17T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("weeks"), l(-120), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2053-06-22T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("weekday"), l(12345), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("1985-11-16T04:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("weekday"), l(-12345), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2020-07-05T05:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("hours"), l(7321), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2018-11-03T03:10:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("hours"), l(-7321), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2021-07-21T01:04:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("minute"), l(987654), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2017-10-18T07:16:37.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("minute"), l(-987654), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2020-02-01T11:51:31.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("seconds"), l(12987654), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2019-04-06T20:29:43.123456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("seconds"), l(-12987654), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2019-09-19T04:56:42.555456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("ms"), l(1298765432), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2019-08-20T03:24:31.691456789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("ms"), l(-1298765432), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2019-09-04T04:12:41.111110789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("mcs"), l(123987654), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2019-09-04T04:08:33.135802789+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("mcs"), l(-123987654), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + + assertEquals("2019-09-04T04:10:37.935855554+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("nanoseconds"), l(812398765), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + assertEquals("2019-09-04T04:10:36.311058024+10:00", + toString((ZonedDateTime) new DateAdd(Source.EMPTY, l("nanoseconds"), l(-812398765), dateTime, zoneId) + .makePipe().asProcessor().process(null))); + } + + private String toString(ZonedDateTime dateTime) { + return ISO_DATE_WITH_NANOS.format(dateTime); + } +} 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 d46afb3e157..32e336161ee 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 @@ -102,7 +102,7 @@ public class QueryTranslatorTests extends ESTestCase { private LogicalPlan plan(String sql) { return analyzer.analyze(parser.createStatement(sql), true); } - + private PhysicalPlan optimizeAndPlan(String sql) { return planner.plan(optimizer.optimize(plan(sql)), true); } @@ -134,7 +134,7 @@ public class QueryTranslatorTests extends ESTestCase { assertEquals("int", tq.term()); assertEquals(5, tq.value()); } - + public void testTermEqualityForDate() { LogicalPlan p = plan("SELECT some.string FROM test WHERE date = 5"); assertTrue(p instanceof Project); @@ -148,7 +148,7 @@ public class QueryTranslatorTests extends ESTestCase { assertEquals("date", tq.term()); assertEquals(5, tq.value()); } - + public void testTermEqualityForDateWithLiteralDate() { LogicalPlan p = plan("SELECT some.string FROM test WHERE date = CAST('2019-08-08T12:34:56' AS DATETIME)"); assertTrue(p instanceof Project); @@ -166,7 +166,7 @@ public class QueryTranslatorTests extends ESTestCase { assertTrue(rq.includeUpper()); assertEquals(DATE_FORMAT, rq.format()); } - + public void testTermEqualityForDateWithLiteralTime() { LogicalPlan p = plan("SELECT some.string FROM test WHERE date = CAST('12:34:56' AS TIME)"); assertTrue(p instanceof Project); @@ -236,27 +236,27 @@ public class QueryTranslatorTests extends ESTestCase { assertEquals("date", rq.field()); assertEquals("1969-05-13T12:34:56.000Z", rq.lower()); } - + public void testDateRangeWithCurrentTimestamp() { testDateRangeWithCurrentFunctions("CURRENT_TIMESTAMP()", DATE_FORMAT, TestUtils.TEST_CFG.now()); } - + public void testDateRangeWithCurrentDate() { testDateRangeWithCurrentFunctions("CURRENT_DATE()", DATE_FORMAT, DateUtils.asDateOnly(TestUtils.TEST_CFG.now())); } - + public void testDateRangeWithToday() { testDateRangeWithCurrentFunctions("TODAY()", DATE_FORMAT, DateUtils.asDateOnly(TestUtils.TEST_CFG.now())); } - + public void testDateRangeWithNow() { testDateRangeWithCurrentFunctions("NOW()", DATE_FORMAT, TestUtils.TEST_CFG.now()); } - + public void testDateRangeWithCurrentTime() { testDateRangeWithCurrentFunctions("CURRENT_TIME()", TIME_FORMAT, TestUtils.TEST_CFG.now()); } - + private void testDateRangeWithCurrentFunctions(String function, String pattern, ZonedDateTime now) { String operator = randomFrom(new String[] {">", ">=", "<", "<=", "=", "!="}); LogicalPlan p = plan("SELECT some.string FROM test WHERE date" + operator + function); @@ -267,7 +267,7 @@ public class QueryTranslatorTests extends ESTestCase { QueryTranslation translation = QueryTranslator.toQuery(condition, false); Query query = translation.query; RangeQuery rq; - + if (operator.equals("!=")) { assertTrue(query instanceof NotQuery); NotQuery nq = (NotQuery) query; @@ -278,7 +278,7 @@ public class QueryTranslatorTests extends ESTestCase { rq = (RangeQuery) query; } assertEquals("date", rq.field()); - + if (operator.contains("<") || operator.equals("=") || operator.equals("!=")) { assertEquals(DateFormatter.forPattern(pattern).format(now.withNano(DateUtils.getNanoPrecision(null, now.getNano()))), rq.upper()); @@ -293,6 +293,23 @@ public class QueryTranslatorTests extends ESTestCase { assertEquals(pattern, rq.format()); } + public void testTranslateDateAdd_WhereClause_Painless() { + LogicalPlan p = plan("SELECT int FROM test WHERE DATE_ADD('quarter',int, date) > '2018-09-04'::date"); + 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("InternalSqlScriptUtils.nullSafeFilter(InternalSqlScriptUtils.gt(InternalSqlScriptUtils.dateAdd(" + + "params.v0,InternalSqlScriptUtils.docValue(doc,params.v1),InternalSqlScriptUtils.docValue(doc,params.v2)," + + "params.v3),InternalSqlScriptUtils.asDateTime(params.v4)))", + sc.script().toString()); + assertEquals("[{v=quarter}, {v=int}, {v=date}, {v=Z}, {v=2018-09-04T00:00:00.000Z}]", sc.script().params().toString()); + } + public void testTranslateDateTrunc_WhereClause_Painless() { LogicalPlan p = plan("SELECT int FROM test WHERE DATE_TRUNC('month', date) > '2018-09-04'::date"); assertTrue(p instanceof Project); @@ -336,7 +353,7 @@ public class QueryTranslatorTests extends ESTestCase { WildcardQuery qsq = ((WildcardQuery) qt.query); assertEquals("some.string.typical", qsq.field()); } - + public void testRLikeOnInexact() { LogicalPlan p = plan("SELECT * FROM test WHERE some.string RLIKE '.*a.*'"); assertTrue(p instanceof Project); @@ -348,7 +365,7 @@ public class QueryTranslatorTests extends ESTestCase { RegexQuery qsq = ((RegexQuery) qt.query); assertEquals("some.string.typical", qsq.field()); } - + public void testLikeConstructsNotSupported() { LogicalPlan p = plan("SELECT LTRIM(keyword) lt FROM test WHERE LTRIM(keyword) like '%a%'"); assertTrue(p instanceof Project); @@ -358,7 +375,7 @@ public class QueryTranslatorTests extends ESTestCase { SqlIllegalArgumentException ex = expectThrows(SqlIllegalArgumentException.class, () -> QueryTranslator.toQuery(condition, false)); assertEquals("Scalar function [LTRIM(keyword)] not allowed (yet) as argument for LIKE", ex.getMessage()); } - + public void testRLikeConstructsNotSupported() { LogicalPlan p = plan("SELECT LTRIM(keyword) lt FROM test WHERE LTRIM(keyword) RLIKE '.*a.*'"); assertTrue(p instanceof Project); @@ -368,13 +385,13 @@ public class QueryTranslatorTests extends ESTestCase { SqlIllegalArgumentException ex = expectThrows(SqlIllegalArgumentException.class, () -> QueryTranslator.toQuery(condition, false)); assertEquals("Scalar function [LTRIM(keyword)] not allowed (yet) as argument for RLIKE", ex.getMessage()); } - + public void testDifferentLikeAndNotLikePatterns() { LogicalPlan p = plan("SELECT keyword k FROM test WHERE k LIKE 'X%' AND k NOT LIKE 'Y%'"); assertTrue(p instanceof Project); p = ((Project) p).child(); assertTrue(p instanceof Filter); - + Expression condition = ((Filter) p).condition(); QueryTranslation qt = QueryTranslator.toQuery(condition, false); assertEquals(BoolQuery.class, qt.query.getClass()); @@ -382,18 +399,18 @@ public class QueryTranslatorTests extends ESTestCase { assertTrue(bq.isAnd()); assertTrue(bq.left() instanceof WildcardQuery); assertTrue(bq.right() instanceof NotQuery); - + NotQuery nq = (NotQuery) bq.right(); assertTrue(nq.child() instanceof WildcardQuery); WildcardQuery lqsq = (WildcardQuery) bq.left(); WildcardQuery rqsq = (WildcardQuery) nq.child(); - + assertEquals("X*", lqsq.query()); assertEquals("keyword", lqsq.field()); assertEquals("Y*", rqsq.query()); assertEquals("keyword", rqsq.field()); } - + public void testRLikePatterns() { String[] patterns = new String[] {"(...)+", "abab(ab)?", "(ab){1,2}", "(ab){3}", "aabb|bbaa", "a+b+|b+a+", "aa(cc|bb)", "a{4,6}b{4,6}", ".{3}.{3}", "aaa*bbb*", "a+.+", "a.c.e", "[^abc\\-]"}; @@ -401,13 +418,13 @@ public class QueryTranslatorTests extends ESTestCase { assertDifferentRLikeAndNotRLikePatterns(randomFrom(patterns), randomFrom(patterns)); } } - + private void assertDifferentRLikeAndNotRLikePatterns(String firstPattern, String secondPattern) { LogicalPlan p = plan("SELECT keyword k FROM test WHERE k RLIKE '" + firstPattern + "' AND k NOT RLIKE '" + secondPattern + "'"); assertTrue(p instanceof Project); p = ((Project) p).child(); assertTrue(p instanceof Filter); - + Expression condition = ((Filter) p).condition(); QueryTranslation qt = QueryTranslator.toQuery(condition, false); assertEquals(BoolQuery.class, qt.query.getClass()); @@ -415,12 +432,12 @@ public class QueryTranslatorTests extends ESTestCase { assertTrue(bq.isAnd()); assertTrue(bq.left() instanceof RegexQuery); assertTrue(bq.right() instanceof NotQuery); - + NotQuery nq = (NotQuery) bq.right(); assertTrue(nq.child() instanceof RegexQuery); RegexQuery lqsq = (RegexQuery) bq.left(); RegexQuery rqsq = (RegexQuery) nq.child(); - + assertEquals(firstPattern, lqsq.regex()); assertEquals("keyword", lqsq.field()); assertEquals(secondPattern, rqsq.regex()); @@ -646,7 +663,7 @@ public class QueryTranslatorTests extends ESTestCase { assertThat(aggFilter.scriptTemplate().params().toString(), startsWith("[{a=max(int){a->")); assertThat(aggFilter.scriptTemplate().params().toString(), endsWith(", {v=10}]")); } - + public void testTranslateRoundWithOneParameter() { LogicalPlan p = plan("SELECT ROUND(YEAR(date)) FROM test GROUP BY ROUND(YEAR(date))"); @@ -667,16 +684,16 @@ public class QueryTranslatorTests extends ESTestCase { scriptTemplate.toString()); assertEquals("[{v=date}, {v=Z}, {v=YEAR}, {v=null}]", scriptTemplate.params().toString()); } - + public void testTranslateRoundWithTwoParameters() { LogicalPlan p = plan("SELECT ROUND(YEAR(date), -2) FROM test GROUP BY ROUND(YEAR(date), -2)"); - + assertTrue(p instanceof Aggregate); assertEquals(1, ((Aggregate) p).groupings().size()); assertEquals(1, ((Aggregate) p).aggregates().size()); assertTrue(((Aggregate) p).groupings().get(0) instanceof Round); assertTrue(((Aggregate) p).aggregates().get(0) instanceof Round); - + Round groupingRound = (Round) ((Aggregate) p).groupings().get(0); assertEquals(2, groupingRound.children().size()); assertTrue(groupingRound.children().get(1) instanceof Literal); @@ -905,7 +922,7 @@ public class QueryTranslatorTests extends ESTestCase { containsString("\"date_histogram\":{\"field\":\"date\",\"missing_bucket\":true,\"value_type\":\"date\",\"order\":\"asc\"," + "\"fixed_interval\":\"62208000000ms\",\"time_zone\":\"Z\"}}}]}")); } - + public void testGroupByYearQueryTranslator() { PhysicalPlan p = optimizeAndPlan("SELECT YEAR(date) FROM test GROUP BY YEAR(date)"); assertEquals(EsQueryExec.class, p.getClass()); @@ -971,7 +988,7 @@ public class QueryTranslatorTests extends ESTestCase { assertEquals(2, ee.output().size()); assertThat(ee.output().get(0).toString(), startsWith("dkey{a->")); assertThat(ee.output().get(1).toString(), startsWith("key{a->")); - + Collection subAggs = ee.queryContainer().aggs().asAggBuilder().getSubAggregations(); assertEquals(2, subAggs.size()); assertTrue(subAggs.toArray()[0] instanceof CardinalityAggregationBuilder); @@ -979,15 +996,15 @@ public class QueryTranslatorTests extends ESTestCase { CardinalityAggregationBuilder cardinalityKeyword = (CardinalityAggregationBuilder) subAggs.toArray()[0]; assertEquals("keyword", cardinalityKeyword.field()); - + FilterAggregationBuilder existsKeyword = (FilterAggregationBuilder) subAggs.toArray()[1]; assertTrue(existsKeyword.getFilter() instanceof ExistsQueryBuilder); assertEquals("keyword", ((ExistsQueryBuilder) existsKeyword.getFilter()).fieldName()); - + assertThat(ee.queryContainer().aggs().asAggBuilder().toString().replaceAll("\\s+", ""), endsWith("{\"filter\":{\"exists\":{\"field\":\"keyword\",\"boost\":1.0}}}}}}")); } - + public void testAllCountVariantsWithHavingGenerateCorrectAggregations() { PhysicalPlan p = optimizeAndPlan("SELECT AVG(int), COUNT(keyword) ln, COUNT(distinct keyword) dln, COUNT(some.dotted.field) fn," + "COUNT(distinct some.dotted.field) dfn, COUNT(*) ccc FROM test GROUP BY bool " @@ -1001,7 +1018,7 @@ public class QueryTranslatorTests extends ESTestCase { assertThat(ee.output().get(3).toString(), startsWith("fn{a->")); assertThat(ee.output().get(4).toString(), startsWith("dfn{a->")); assertThat(ee.output().get(5).toString(), startsWith("ccc{a->")); - + Collection subAggs = ee.queryContainer().aggs().asAggBuilder().getSubAggregations(); assertEquals(5, subAggs.size()); assertTrue(subAggs.toArray()[0] instanceof AvgAggregationBuilder); @@ -1009,21 +1026,21 @@ public class QueryTranslatorTests extends ESTestCase { assertTrue(subAggs.toArray()[2] instanceof CardinalityAggregationBuilder); assertTrue(subAggs.toArray()[3] instanceof FilterAggregationBuilder); assertTrue(subAggs.toArray()[4] instanceof CardinalityAggregationBuilder); - + AvgAggregationBuilder avgInt = (AvgAggregationBuilder) subAggs.toArray()[0]; assertEquals("int", avgInt.field()); - + FilterAggregationBuilder existsKeyword = (FilterAggregationBuilder) subAggs.toArray()[1]; assertTrue(existsKeyword.getFilter() instanceof ExistsQueryBuilder); assertEquals("keyword", ((ExistsQueryBuilder) existsKeyword.getFilter()).fieldName()); - + CardinalityAggregationBuilder cardinalityKeyword = (CardinalityAggregationBuilder) subAggs.toArray()[2]; assertEquals("keyword", cardinalityKeyword.field()); - + FilterAggregationBuilder existsDottedField = (FilterAggregationBuilder) subAggs.toArray()[3]; assertTrue(existsDottedField.getFilter() instanceof ExistsQueryBuilder); assertEquals("some.dotted.field", ((ExistsQueryBuilder) existsDottedField.getFilter()).fieldName()); - + CardinalityAggregationBuilder cardinalityDottedField = (CardinalityAggregationBuilder) subAggs.toArray()[4]; assertEquals("some.dotted.field", cardinalityDottedField.field()); @@ -1193,7 +1210,7 @@ public class QueryTranslatorTests extends ESTestCase { + "\"lang\":\"painless\"," + "\"params\":{\"v0\":\"date\",\"v1\":\"P1Y\",\"v2\":\"INTERVAL_YEAR\",\"v3\":\"2019-03-11T12:34:56.000Z\"}},")); } - + public void testChronoFieldBasedDateTimeFunctionsWithMathIntervalAndGroupBy() { DateTimeExtractor randomFunction = randomValueOtherThan(DateTimeExtractor.YEAR, () -> randomFrom(DateTimeExtractor.values())); PhysicalPlan p = optimizeAndPlan( @@ -1210,7 +1227,7 @@ public class QueryTranslatorTests extends ESTestCase { + "\"v3\":\"Z\",\"v4\":\"" + randomFunction.chronoField().name() + "\"}},\"missing_bucket\":true," + "\"value_type\":\"long\",\"order\":\"asc\"}}}]}}}}")); } - + public void testDateTimeFunctionsWithMathIntervalAndGroupBy() { String[] functions = new String[] {"DAY_NAME", "MONTH_NAME", "DAY_OF_WEEK", "WEEK_OF_YEAR", "QUARTER"}; String[] scriptMethods = new String[] {"dayName", "monthName", "dayOfWeek", "weekOfYear", "quarter"};