diff --git a/docs/reference/query-languages/esql/_snippets/functions/description/day_name.md b/docs/reference/query-languages/esql/_snippets/functions/description/day_name.md new file mode 100644 index 0000000000000..3153a68b54da6 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/description/day_name.md @@ -0,0 +1,6 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Description** + +Returns the name of the weekday for date based on the configured Locale. + diff --git a/docs/reference/query-languages/esql/_snippets/functions/examples/day_name.md b/docs/reference/query-languages/esql/_snippets/functions/examples/day_name.md new file mode 100644 index 0000000000000..5c246f632f223 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/examples/day_name.md @@ -0,0 +1,10 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Example** + +```esql +ROW dt = to_datetime("1953-09-02T00:00:00.000Z") +| EVAL weekday = DAY_NAME(dt); +``` + + diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/day_name.md b/docs/reference/query-languages/esql/_snippets/functions/layout/day_name.md new file mode 100644 index 0000000000000..4cd3f1ec90c30 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/day_name.md @@ -0,0 +1,26 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +## `DAY_NAME` [esql-day_name] +```{applies_to} +stack: ga 9.2.0 +``` + +**Syntax** + +:::{image} ../../../images/functions/day_name.svg +:alt: Embedded +:class: text-center +::: + + +:::{include} ../parameters/day_name.md +::: + +:::{include} ../description/day_name.md +::: + +:::{include} ../types/day_name.md +::: + +:::{include} ../examples/day_name.md +::: diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/day_name.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/day_name.md new file mode 100644 index 0000000000000..43182ad4b2d26 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/day_name.md @@ -0,0 +1,7 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Parameters** + +`date` +: Date expression. If `null`, the function returns `null`. + diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/day_name.md b/docs/reference/query-languages/esql/_snippets/functions/types/day_name.md new file mode 100644 index 0000000000000..097955acd2cfa --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/types/day_name.md @@ -0,0 +1,9 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Supported types** + +| date | result | +| --- | --- | +| date | keyword | +| date_nanos | keyword | + diff --git a/docs/reference/query-languages/esql/_snippets/lists/date-time-functions.md b/docs/reference/query-languages/esql/_snippets/lists/date-time-functions.md index a9b6b090e0472..b59db4d50bf58 100644 --- a/docs/reference/query-languages/esql/_snippets/lists/date-time-functions.md +++ b/docs/reference/query-languages/esql/_snippets/lists/date-time-functions.md @@ -3,4 +3,5 @@ * [`DATE_FORMAT`](../../functions-operators/date-time-functions.md#esql-date_format) * [`DATE_PARSE`](../../functions-operators/date-time-functions.md#esql-date_parse) * [`DATE_TRUNC`](../../functions-operators/date-time-functions.md#esql-date_trunc) +* [`DAY_NAME`](../../functions-operators/date-time-functions.md#esql-day_name) * [`NOW`](../../functions-operators/date-time-functions.md#esql-now) diff --git a/docs/reference/query-languages/esql/functions-operators/date-time-functions.md b/docs/reference/query-languages/esql/functions-operators/date-time-functions.md index f11a3a76e0f75..4823468b0bb97 100644 --- a/docs/reference/query-languages/esql/functions-operators/date-time-functions.md +++ b/docs/reference/query-languages/esql/functions-operators/date-time-functions.md @@ -28,6 +28,9 @@ mapped_pages: :::{include} ../_snippets/functions/layout/date_trunc.md ::: +:::{include} ../_snippets/functions/layout/day_name.md +::: + :::{include} ../_snippets/functions/layout/now.md ::: diff --git a/docs/reference/query-languages/esql/images/functions/day_name.svg b/docs/reference/query-languages/esql/images/functions/day_name.svg new file mode 100644 index 0000000000000..6bf8cdf0e4951 --- /dev/null +++ b/docs/reference/query-languages/esql/images/functions/day_name.svg @@ -0,0 +1 @@ +DAY_NAME(date) \ No newline at end of file diff --git a/docs/reference/query-languages/esql/kibana/definition/functions/day_name.json b/docs/reference/query-languages/esql/kibana/definition/functions/day_name.json new file mode 100644 index 0000000000000..3f2933b5231f6 --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/definition/functions/day_name.json @@ -0,0 +1,37 @@ +{ + "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it.", + "type" : "scalar", + "name" : "day_name", + "description" : "Returns the name of the weekday for date based on the configured Locale.", + "signatures" : [ + { + "params" : [ + { + "name" : "date", + "type" : "date", + "optional" : false, + "description" : "Date expression. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "keyword" + }, + { + "params" : [ + { + "name" : "date", + "type" : "date_nanos", + "optional" : false, + "description" : "Date expression. If `null`, the function returns `null`." + } + ], + "variadic" : false, + "returnType" : "keyword" + } + ], + "examples" : [ + "ROW dt = to_datetime(\"1953-09-02T00:00:00.000Z\")\n| EVAL weekday = DAY_NAME(dt);" + ], + "preview" : false, + "snapshot_only" : false +} diff --git a/docs/reference/query-languages/esql/kibana/docs/functions/day_name.md b/docs/reference/query-languages/esql/kibana/docs/functions/day_name.md new file mode 100644 index 0000000000000..ff8371538ba55 --- /dev/null +++ b/docs/reference/query-languages/esql/kibana/docs/functions/day_name.md @@ -0,0 +1,9 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +### DAY NAME +Returns the name of the weekday for date based on the configured Locale. + +```esql +ROW dt = to_datetime("1953-09-02T00:00:00.000Z") +| EVAL weekday = DAY_NAME(dt); +``` diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec index 788a5f9877dea..2b97b2985e7b9 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec @@ -1836,3 +1836,97 @@ y:date | c:long 1987-07-01T00:00:00.000Z | 1 1987-08-01T00:00:00.000Z | 2 ; + +dayNameRowTest +required_capability:fn_day_name +// tag::docsDayName[] +ROW dt = to_datetime("1953-09-02T00:00:00.000Z") +| EVAL weekday = DAY_NAME(dt); +// end::docsDayName[] + +dt:date | weekday:keyword +1953-09-02T00:00:00.000Z | Wednesday +; + +dayNameSimple +required_capability:fn_day_name +from employees +| sort emp_no +| keep emp_no, hire_date +| eval day = day_name(hire_date) +| limit 1; + +emp_no:integer | hire_date:date | day:keyword +10001 | 1986-06-26T00:00:00.000Z | Thursday +; + +dayNameTruncate +required_capability:fn_day_name +FROM employees +| EVAL a = hire_date +| RENAME hire_date as b +| WHERE a > "1987-01-01" and a < "1988-01-01" +| EVAL y = date_trunc(1 month, b) +| STATS c = count(emp_no) by y +| EVAL week_day = day_name(y) +| SORT y +| KEEP y, c, week_day +| LIMIT 5; + +y:date | c:long | week_day:keyword +1987-03-01T00:00:00.000Z | 5 | Sunday +1987-04-01T00:00:00.000Z | 3 | Wednesday +1987-05-01T00:00:00.000Z | 1 | Friday +1987-07-01T00:00:00.000Z | 1 | Wednesday +1987-08-01T00:00:00.000Z | 2 | Saturday +; + +dayNameNestedCall +required_capability:fn_day_name +from employees +| sort emp_no +| eval first_day_of_month = day_name(date_trunc(1 month, hire_date)) +| keep emp_no, hire_date, first_day_of_month +| limit 10; + +emp_no:integer | hire_date:datetime | first_day_of_month:keyword +10001 | 1986-06-26T00:00:00.000Z | Sunday +10002 | 1985-11-21T00:00:00.000Z | Friday +10003 | 1986-08-28T00:00:00.000Z | Friday +10004 | 1986-12-01T00:00:00.000Z | Monday +10005 | 1989-09-12T00:00:00.000Z | Friday +10006 | 1989-06-02T00:00:00.000Z | Thursday +10007 | 1989-02-10T00:00:00.000Z | Wednesday +10008 | 1994-09-15T00:00:00.000Z | Thursday +10009 | 1985-02-18T00:00:00.000Z | Friday +10010 | 1989-08-24T00:00:00.000Z | Tuesday +; + +dayNameNestedCall +required_capability:fn_day_name +from employees +| sort emp_no desc +| eval first_day_of_month = to_upper(day_name(date_trunc(1 month, hire_date))) +| keep emp_no, hire_date, first_day_of_month +| limit 10; + +emp_no:integer | hire_date:datetime | first_day_of_month:keyword +10100 | 1987-09-21T00:00:00.000Z | TUESDAY +10099 | 1988-10-18T00:00:00.000Z | SATURDAY +10098 | 1985-05-13T00:00:00.000Z | WEDNESDAY +10097 | 1990-09-15T00:00:00.000Z | SATURDAY +10096 | 1990-01-14T00:00:00.000Z | MONDAY +10095 | 1986-07-15T00:00:00.000Z | TUESDAY +10094 | 1987-04-18T00:00:00.000Z | WEDNESDAY +10093 | 1996-11-05T00:00:00.000Z | FRIDAY +10092 | 1989-09-22T00:00:00.000Z | FRIDAY +10091 | 1992-11-18T00:00:00.000Z | SUNDAY +; + +dayNameNull +required_capability:fn_day_name +from employees | where emp_no == 10040 | eval x = day_name(birth_date) | keep emp_no, birth_date, hire_date, x; + +emp_no:integer | birth_date:date | hire_date:date | x:keyword +10040 | null | 1993-02-14T00:00:00.000Z | null +; diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameMillisEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameMillisEvaluator.java new file mode 100644 index 0000000000000..9edbe5d1146df --- /dev/null +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameMillisEvaluator.java @@ -0,0 +1,139 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.xpack.esql.expression.function.scalar.date; + +import java.lang.IllegalArgumentException; +import java.lang.Override; +import java.lang.String; +import java.time.ZoneId; +import java.util.Locale; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.LongBlock; +import org.elasticsearch.compute.data.LongVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.compute.operator.Warnings; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.xpack.esql.core.tree.Source; + +/** + * {@link EvalOperator.ExpressionEvaluator} implementation for {@link DayName}. + * This class is generated. Edit {@code EvaluatorImplementer} instead. + */ +public final class DayNameMillisEvaluator implements EvalOperator.ExpressionEvaluator { + private final Source source; + + private final EvalOperator.ExpressionEvaluator val; + + private final ZoneId zoneId; + + private final Locale locale; + + private final DriverContext driverContext; + + private Warnings warnings; + + public DayNameMillisEvaluator(Source source, EvalOperator.ExpressionEvaluator val, ZoneId zoneId, + Locale locale, DriverContext driverContext) { + this.source = source; + this.val = val; + this.zoneId = zoneId; + this.locale = locale; + this.driverContext = driverContext; + } + + @Override + public Block eval(Page page) { + try (LongBlock valBlock = (LongBlock) val.eval(page)) { + LongVector valVector = valBlock.asVector(); + if (valVector == null) { + return eval(page.getPositionCount(), valBlock); + } + return eval(page.getPositionCount(), valVector).asBlock(); + } + } + + public BytesRefBlock eval(int positionCount, LongBlock valBlock) { + try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) { + position: for (int p = 0; p < positionCount; p++) { + if (valBlock.isNull(p)) { + result.appendNull(); + continue position; + } + if (valBlock.getValueCount(p) != 1) { + if (valBlock.getValueCount(p) > 1) { + warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value")); + } + result.appendNull(); + continue position; + } + result.appendBytesRef(DayName.processMillis(valBlock.getLong(valBlock.getFirstValueIndex(p)), this.zoneId, this.locale)); + } + return result.build(); + } + } + + public BytesRefVector eval(int positionCount, LongVector valVector) { + try(BytesRefVector.Builder result = driverContext.blockFactory().newBytesRefVectorBuilder(positionCount)) { + position: for (int p = 0; p < positionCount; p++) { + result.appendBytesRef(DayName.processMillis(valVector.getLong(p), this.zoneId, this.locale)); + } + return result.build(); + } + } + + @Override + public String toString() { + return "DayNameMillisEvaluator[" + "val=" + val + ", zoneId=" + zoneId + ", locale=" + locale + "]"; + } + + @Override + public void close() { + Releasables.closeExpectNoException(val); + } + + private Warnings warnings() { + if (warnings == null) { + this.warnings = Warnings.createWarnings( + driverContext.warningsMode(), + source.source().getLineNumber(), + source.source().getColumnNumber(), + source.text() + ); + } + return warnings; + } + + static class Factory implements EvalOperator.ExpressionEvaluator.Factory { + private final Source source; + + private final EvalOperator.ExpressionEvaluator.Factory val; + + private final ZoneId zoneId; + + private final Locale locale; + + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory val, ZoneId zoneId, + Locale locale) { + this.source = source; + this.val = val; + this.zoneId = zoneId; + this.locale = locale; + } + + @Override + public DayNameMillisEvaluator get(DriverContext context) { + return new DayNameMillisEvaluator(source, val.get(context), zoneId, locale, context); + } + + @Override + public String toString() { + return "DayNameMillisEvaluator[" + "val=" + val + ", zoneId=" + zoneId + ", locale=" + locale + "]"; + } + } +} diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameNanosEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameNanosEvaluator.java new file mode 100644 index 0000000000000..9f6b955c7787b --- /dev/null +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameNanosEvaluator.java @@ -0,0 +1,139 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.xpack.esql.expression.function.scalar.date; + +import java.lang.IllegalArgumentException; +import java.lang.Override; +import java.lang.String; +import java.time.ZoneId; +import java.util.Locale; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.LongBlock; +import org.elasticsearch.compute.data.LongVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.compute.operator.Warnings; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.xpack.esql.core.tree.Source; + +/** + * {@link EvalOperator.ExpressionEvaluator} implementation for {@link DayName}. + * This class is generated. Edit {@code EvaluatorImplementer} instead. + */ +public final class DayNameNanosEvaluator implements EvalOperator.ExpressionEvaluator { + private final Source source; + + private final EvalOperator.ExpressionEvaluator val; + + private final ZoneId zoneId; + + private final Locale locale; + + private final DriverContext driverContext; + + private Warnings warnings; + + public DayNameNanosEvaluator(Source source, EvalOperator.ExpressionEvaluator val, ZoneId zoneId, + Locale locale, DriverContext driverContext) { + this.source = source; + this.val = val; + this.zoneId = zoneId; + this.locale = locale; + this.driverContext = driverContext; + } + + @Override + public Block eval(Page page) { + try (LongBlock valBlock = (LongBlock) val.eval(page)) { + LongVector valVector = valBlock.asVector(); + if (valVector == null) { + return eval(page.getPositionCount(), valBlock); + } + return eval(page.getPositionCount(), valVector).asBlock(); + } + } + + public BytesRefBlock eval(int positionCount, LongBlock valBlock) { + try(BytesRefBlock.Builder result = driverContext.blockFactory().newBytesRefBlockBuilder(positionCount)) { + position: for (int p = 0; p < positionCount; p++) { + if (valBlock.isNull(p)) { + result.appendNull(); + continue position; + } + if (valBlock.getValueCount(p) != 1) { + if (valBlock.getValueCount(p) > 1) { + warnings().registerException(new IllegalArgumentException("single-value function encountered multi-value")); + } + result.appendNull(); + continue position; + } + result.appendBytesRef(DayName.processNanos(valBlock.getLong(valBlock.getFirstValueIndex(p)), this.zoneId, this.locale)); + } + return result.build(); + } + } + + public BytesRefVector eval(int positionCount, LongVector valVector) { + try(BytesRefVector.Builder result = driverContext.blockFactory().newBytesRefVectorBuilder(positionCount)) { + position: for (int p = 0; p < positionCount; p++) { + result.appendBytesRef(DayName.processNanos(valVector.getLong(p), this.zoneId, this.locale)); + } + return result.build(); + } + } + + @Override + public String toString() { + return "DayNameNanosEvaluator[" + "val=" + val + ", zoneId=" + zoneId + ", locale=" + locale + "]"; + } + + @Override + public void close() { + Releasables.closeExpectNoException(val); + } + + private Warnings warnings() { + if (warnings == null) { + this.warnings = Warnings.createWarnings( + driverContext.warningsMode(), + source.source().getLineNumber(), + source.source().getColumnNumber(), + source.text() + ); + } + return warnings; + } + + static class Factory implements EvalOperator.ExpressionEvaluator.Factory { + private final Source source; + + private final EvalOperator.ExpressionEvaluator.Factory val; + + private final ZoneId zoneId; + + private final Locale locale; + + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory val, ZoneId zoneId, + Locale locale) { + this.source = source; + this.val = val; + this.zoneId = zoneId; + this.locale = locale; + } + + @Override + public DayNameNanosEvaluator get(DriverContext context) { + return new DayNameNanosEvaluator(source, val.get(context), zoneId, locale, context); + } + + @Override + public String toString() { + return "DayNameNanosEvaluator[" + "val=" + val + ", zoneId=" + zoneId + ", locale=" + locale + "]"; + } + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 6a2b112b58deb..3b62a43b6d9d8 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -235,6 +235,11 @@ public enum Cap { */ FN_SCALB, + /** + * Support for function DAY_NAME + */ + FN_DAY_NAME, + /** * Fixes for multiple functions not serializing their source, and emitting warnings with wrong line number and text. */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index 649503b1443d2..959602cc4e20d 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -85,6 +85,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateFormat; import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateParse; import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc; +import org.elasticsearch.xpack.esql.expression.function.scalar.date.DayName; import org.elasticsearch.xpack.esql.expression.function.scalar.date.Now; import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch; import org.elasticsearch.xpack.esql.expression.function.scalar.ip.IpPrefix; @@ -390,6 +391,7 @@ private static FunctionDefinition[][] functions() { def(DateFormat.class, DateFormat::new, "date_format"), def(DateParse.class, DateParse::new, "date_parse"), def(DateTrunc.class, DateTrunc::new, "date_trunc"), + def(DayName.class, DayName::new, "day_name"), def(Now.class, Now::new, "now") }, // spatial new FunctionDefinition[] { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java index 7913d5e11a5a5..684e685e2eafb 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/ScalarFunctionWritables.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateFormat; import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateParse; import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc; +import org.elasticsearch.xpack.esql.expression.function.scalar.date.DayName; import org.elasticsearch.xpack.esql.expression.function.scalar.date.Now; import org.elasticsearch.xpack.esql.expression.function.scalar.ip.CIDRMatch; import org.elasticsearch.xpack.esql.expression.function.scalar.ip.IpPrefix; @@ -82,6 +83,7 @@ public static List getNamedWriteables() { entries.add(DateFormat.ENTRY); entries.add(DateParse.ENTRY); entries.add(DateTrunc.ENTRY); + entries.add(DayName.ENTRY); entries.add(IpPrefix.ENTRY); entries.add(Least.ENTRY); entries.add(Left.ENTRY); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayName.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayName.java new file mode 100644 index 0000000000000..a5872d105180c --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayName.java @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.date; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.compute.ann.Evaluator; +import org.elasticsearch.compute.ann.Fixed; +import org.elasticsearch.compute.operator.EvalOperator; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; +import org.elasticsearch.xpack.esql.core.tree.NodeInfo; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.Example; +import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; +import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; +import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlConfigurationFunction; +import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; +import org.elasticsearch.xpack.esql.session.Configuration; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.TextStyle; +import java.util.List; +import java.util.Locale; + +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; + +public class DayName extends EsqlConfigurationFunction { + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "DayName", DayName::new); + + private final Expression field; + + @FunctionInfo( + returnType = "keyword", + description = "Returns the name of the weekday for date based on the configured Locale.", + examples = @Example(file = "date", tag = "docsDayName"), + appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.GA, version = "9.2.0") } + ) + public DayName( + Source source, + @Param( + name = "date", + type = { "date", "date_nanos" }, + description = "Date expression. If `null`, the function returns `null`." + ) Expression date, + Configuration configuration + ) { + super(source, List.of(date), configuration); + this.field = date; + } + + private DayName(StreamInput in) throws IOException { + this(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class), ((PlanStreamInput) in).configuration()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + source().writeTo(out); + out.writeNamedWriteable(field); + } + + @Override + public String getWriteableName() { + return ENTRY.name; + } + + Expression field() { + return field; + } + + @Override + public DataType dataType() { + return DataType.KEYWORD; + } + + @Override + protected TypeResolution resolveType() { + if (childrenResolved() == false) { + return new TypeResolution("Unresolved children"); + } + + String operationName = sourceText(); + TypeResolution resolution = TypeResolutions.isType(field, DataType::isDate, operationName, FIRST, "datetime or date_nanos"); + if (resolution.unresolved()) { + return resolution; + } + + return TypeResolution.TYPE_RESOLVED; + } + + @Override + public boolean foldable() { + return field.foldable(); + } + + @Override + public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { + var fieldEvaluator = toEvaluator.apply(field); + if (field().dataType() == DataType.DATE_NANOS) { + return new DayNameNanosEvaluator.Factory(source(), fieldEvaluator, configuration().zoneId(), configuration().locale()); + } + return new DayNameMillisEvaluator.Factory(source(), fieldEvaluator, configuration().zoneId(), configuration().locale()); + } + + @Override + public Expression replaceChildren(List newChildren) { + return new DayName(source(), newChildren.get(0), configuration()); + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, DayName::new, field, configuration()); + } + + @Evaluator(extraName = "Millis") + static BytesRef processMillis(long val, @Fixed ZoneId zoneId, @Fixed Locale locale) { + ZonedDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(val), zoneId); + String displayName = dateTime.getDayOfWeek().getDisplayName(TextStyle.FULL, locale); + return new BytesRef(displayName); + } + + @Evaluator(extraName = "Nanos") + static BytesRef processNanos(long val, @Fixed ZoneId zoneId, @Fixed Locale locale) { + ZonedDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(0L, val), zoneId); + String displayName = dateTime.getDayOfWeek().getDisplayName(TextStyle.FULL, locale); + return new BytesRef(displayName); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameErrorTests.java new file mode 100644 index 0000000000000..b47f8dd8ec1cb --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameErrorTests.java @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.date; + +import org.elasticsearch.xpack.esql.EsqlTestUtils; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.ErrorsForCasesWithoutExamplesTestCase; +import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.hamcrest.Matcher; + +import java.util.List; +import java.util.Set; + +import static org.hamcrest.Matchers.equalTo; + +public class DayNameErrorTests extends ErrorsForCasesWithoutExamplesTestCase { + @Override + protected List cases() { + return paramsToSuppliers(DayNameTests.parameters()); + } + + @Override + protected Expression build(Source source, List args) { + return new DayName(source, args.get(0), EsqlTestUtils.TEST_CFG); + } + + @Override + protected Matcher expectedTypeErrorMatcher(List> validPerPosition, List signature) { + // Single argument version + String source = sourceForSignature(signature); + String name = signature.get(0).typeName(); + return equalTo("first argument of [" + source + "] must be [datetime or date_nanos], found value [] type [" + name + "]"); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameSerializationTests.java new file mode 100644 index 0000000000000..49173d4db9c6d --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameSerializationTests.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.date; + +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.expression.AbstractExpressionSerializationTests; + +import java.io.IOException; + +public class DayNameSerializationTests extends AbstractExpressionSerializationTests { + + @Override + protected DayName createTestInstance() { + Source source = randomSource(); + Expression date = randomChild(); + return new DayName(source, date, configuration()); + } + + @Override + protected DayName mutateInstance(DayName instance) throws IOException { + Source source = instance.source(); + Expression date = instance.field(); + return new DayName(source, randomValueOtherThan(date, AbstractExpressionSerializationTests::randomChild), configuration()); + } + +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameTests.java new file mode 100644 index 0000000000000..9d6630d0d9dbd --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DayNameTests.java @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.scalar.date; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.lucene.BytesRefs; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.time.DateUtils; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.expression.FoldContext; +import org.elasticsearch.xpack.esql.core.expression.Literal; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; +import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; +import org.elasticsearch.xpack.esql.plugin.QueryPragmas; +import org.elasticsearch.xpack.esql.session.Configuration; +import org.hamcrest.Matchers; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.TextStyle; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; + +import static org.hamcrest.Matchers.equalTo; + +public class DayNameTests extends AbstractConfigurationFunctionTestCase { + + public DayNameTests(@Name("TestCase") Supplier testCaseSupplier) { + this.testCase = testCaseSupplier.get(); + } + + @ParametersFactory + public static Iterable parameters() { + List suppliers = new ArrayList<>(); + suppliers.addAll(generateTest("2019-03-11T00:00:00.00Z", "Monday")); + suppliers.addAll(generateTest("2022-07-26T23:59:59.99Z", "Tuesday")); + suppliers.addAll(generateTest("2017-10-11T23:12:32.12Z", "Wednesday")); + suppliers.addAll(generateTest("2023-01-05T07:39:01.28Z", "Thursday")); + suppliers.addAll(generateTest("2023-02-17T10:25:33.38Z", "Friday")); + suppliers.addAll(generateTest("2013-06-15T22:55:33.82Z", "Saturday")); + suppliers.addAll(generateTest("2024-08-18T01:01:29.49Z", "Sunday")); + + suppliers.add( + new TestCaseSupplier( + List.of(DataType.DATETIME), + () -> new TestCaseSupplier.TestCase( + List.of(new TestCaseSupplier.TypedData(null, DataType.DATETIME, "date")), + Matchers.startsWith("DayNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), + DataType.KEYWORD, + equalTo(null) + ) + ) + ); + + return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors(true, suppliers); + } + + private static List generateTest(String dateTime, String expectedWeekDay) { + return List.of( + new TestCaseSupplier( + List.of(DataType.DATETIME), + () -> new TestCaseSupplier.TestCase( + List.of(new TestCaseSupplier.TypedData(toMillis(dateTime), DataType.DATETIME, "date")), + Matchers.startsWith("DayNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), + DataType.KEYWORD, + equalTo(new BytesRef(expectedWeekDay)) + ) + ), + new TestCaseSupplier( + List.of(DataType.DATE_NANOS), + () -> new TestCaseSupplier.TestCase( + List.of(new TestCaseSupplier.TypedData(toNanos(dateTime), DataType.DATE_NANOS, "date")), + Matchers.is("DayNameNanosEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), + DataType.KEYWORD, + equalTo(new BytesRef(expectedWeekDay)) + ) + ) + ); + } + + private static long toMillis(String timestamp) { + return Instant.parse(timestamp).toEpochMilli(); + } + + private static long toNanos(String timestamp) { + return DateUtils.toLong(Instant.parse(timestamp)); + } + + @Override + protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { + return new DayName(source, args.get(0), configuration); + } + + public void testRandomLocale() { + long randomMillis = randomMillisUpToYear9999(); + Configuration cfg = configWithZoneAndLocale(randomZone(), randomLocale(random())); + String expected = Instant.ofEpochMilli(randomMillis) + .atZone(cfg.zoneId()) + .getDayOfWeek() + .getDisplayName(TextStyle.FULL, cfg.locale()); + + DayName func = new DayName(Source.EMPTY, new Literal(Source.EMPTY, randomMillis, DataType.DATETIME), cfg); + assertThat(BytesRefs.toBytesRef(expected), equalTo(func.fold(FoldContext.small()))); + } + + public void testFixedLocaleAndTime() { + long randomMillis = toMillis("2019-03-16T00:00:00.00Z"); + Configuration cfg = configWithZoneAndLocale(ZoneId.of("America/Sao_Paulo"), Locale.of("pt", "br")); + String expected = "sexta-feira"; + + DayName func = new DayName(Source.EMPTY, new Literal(Source.EMPTY, randomMillis, DataType.DATETIME), cfg); + assertThat(BytesRefs.toBytesRef(expected), equalTo(func.fold(FoldContext.small()))); + } + + private Configuration configWithZoneAndLocale(ZoneId zone, Locale locale) { + return new Configuration( + zone, + locale, + null, + null, + new QueryPragmas(Settings.EMPTY), + EsqlPlugin.QUERY_RESULT_TRUNCATION_MAX_SIZE.getDefault(Settings.EMPTY), + EsqlPlugin.QUERY_RESULT_TRUNCATION_DEFAULT_SIZE.getDefault(Settings.EMPTY), + "", + false, + Map.of(), + System.nanoTime(), + randomBoolean() + ); + } +}