From f95c2a85cfec136ace08d394c20fcde8be18ca37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 17 Oct 2025 12:51:54 +0200 Subject: [PATCH 01/54] Add capability and YAML tests for request and SET time_zone parameters --- .../xpack/esql/action/EsqlCapabilities.java | 5 + .../rest-api-spec/test/esql/240_timezone.yml | 188 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/240_timezone.yml 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 df06a1109ecc9..b37d1d827997d 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 @@ -1516,6 +1516,11 @@ public enum Cap { */ FIX_FILTER_ORDINALS, + /** + * "time_zone" parameter in request body and in {@code SET "time_zone"="x"} + */ + GLOBAL_TIMEZONE_PARAMETER(Build.current().isSnapshot()), + ; private final boolean enabled; diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/240_timezone.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/240_timezone.yml new file mode 100644 index 0000000000000..c877a65effa19 --- /dev/null +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/240_timezone.yml @@ -0,0 +1,188 @@ +--- +setup: + - requires: + capabilities: + - method: POST + path: /_query + parameters: [ ] + capabilities: [ global_timezone_parameter ] + test_runner_features: [ capabilities ] + reason: "Check timezone capability" + - do: + indices.create: + index: logs + body: + mappings: + properties: + "@timestamp": + type: date + + - do: + bulk: + index: tzs + refresh: true + body: + - { "index": { } } + - { "@timestamp": "2025-05-31T00:00:00Z" } + - { "index": { } } + - { "@timestamp": "2025-05-31T01:00:00Z" } + - { "index": { } } + - { "@timestamp": "2025-05-31T22:00:00Z" } + - { "index": { } } + - { "@timestamp": "2025-05-31T23:00:00Z" } + +--- +Default UTC: + - do: + esql.query: + body: + query: | + FROM tzs + | EVAL + hour=DATE_EXTRACT("hour_of_day", @timestamp), + day_name=DAY_NAME(@timestamp), + month_name=MONTH_NAME(@timestamp) + | SORT @timestamp ASC + | LIMIT 5 + + - length: { columns: 4 } + - match: { columns.0.name: "@timestamp" } + - match: { columns.1.name: "hour" } + - match: { columns.2.name: "day_name" } + - match: { columns.3.name: "month_name" } + + - length: { values: 4 } + - match: { values.0.0: "2025-05-31T00:00:00.000Z" } + - match: { values.0.1: 0 } + - match: { values.0.2: "Saturday" } + - match: { values.0.3: "May" } + - match: { values.1.0: "2025-05-31T01:00:00.000Z" } + - match: { values.1.1: 1 } + - match: { values.1.2: "Saturday" } + - match: { values.1.3: "May" } + - match: { values.2.0: "2025-05-31T22:00:00.000Z" } + - match: { values.2.1: 22 } + - match: { values.2.2: "Saturday" } + - match: { values.2.3: "May" } + - match: { values.3.0: "2025-05-31T23:00:00.000Z" } + - match: { values.3.1: 23 } + - match: { values.3.2: "Saturday" } + - match: { values.3.3: "May" } + +--- +Paris tz with SET: + - do: + esql.query: + body: + query: | + SET time_zone="Europe/Paris"; + FROM tzs + | EVAL + hour=DATE_EXTRACT("hour_of_day", @timestamp), + day_name=DAY_NAME(@timestamp), + month_name=MONTH_NAME(@timestamp) + | SORT @timestamp ASC + | LIMIT 5 + + - length: { columns: 4 } + - match: { columns.0.name: "@timestamp" } + - match: { columns.1.name: "hour" } + - match: { columns.2.name: "day_name" } + - match: { columns.3.name: "month_name" } + + - length: { values: 4 } + - match: { values.0.0: "2025-05-31T00:00:00.000Z" } + - match: { values.0.1: 2 } + - match: { values.0.2: "Saturday" } + - match: { values.0.3: "May" } + - match: { values.1.0: "2025-05-31T01:00:00.000Z" } + - match: { values.1.1: 3 } + - match: { values.1.2: "Saturday" } + - match: { values.1.3: "May" } + - match: { values.2.0: "2025-05-31T22:00:00.000Z" } + - match: { values.2.1: 0 } + - match: { values.2.2: "Sunday" } + - match: { values.2.3: "June" } + - match: { values.3.0: "2025-05-31T23:00:00.000Z" } + - match: { values.3.1: 1 } + - match: { values.3.2: "Sunday" } + - match: { values.3.3: "June" } + +--- +Paris tz with request parameter: + - do: + esql.query: + body: + time_zone: "Europe/Paris" + query: | + FROM tzs + | EVAL + hour=DATE_EXTRACT("hour_of_day", @timestamp), + day_name=DAY_NAME(@timestamp), + month_name=MONTH_NAME(@timestamp) + | SORT @timestamp ASC + | LIMIT 5 + + - length: { columns: 4 } + - match: { columns.0.name: "@timestamp" } + - match: { columns.1.name: "hour" } + - match: { columns.2.name: "day_name" } + - match: { columns.3.name: "month_name" } + + - length: { values: 4 } + - match: { values.0.0: "2025-05-31T00:00:00.000Z" } + - match: { values.0.1: 2 } + - match: { values.0.2: "Saturday" } + - match: { values.0.3: "May" } + - match: { values.1.0: "2025-05-31T01:00:00.000Z" } + - match: { values.1.1: 3 } + - match: { values.1.2: "Saturday" } + - match: { values.1.3: "May" } + - match: { values.2.0: "2025-05-31T22:00:00.000Z" } + - match: { values.2.1: 0 } + - match: { values.2.2: "Sunday" } + - match: { values.2.3: "June" } + - match: { values.3.0: "2025-05-31T23:00:00.000Z" } + - match: { values.3.1: 1 } + - match: { values.3.2: "Sunday" } + - match: { values.3.3: "June" } + +--- +Set overrides request parameter: + - do: + esql.query: + body: + time_zone: "Europe/Paris" + query: | + SET time_zone="+04:00"; + FROM tzs + | EVAL + hour=DATE_EXTRACT("hour_of_day", @timestamp), + day_name=DAY_NAME(@timestamp), + month_name=MONTH_NAME(@timestamp) + | SORT @timestamp ASC + | LIMIT 5 + + - length: { columns: 4 } + - match: { columns.0.name: "@timestamp" } + - match: { columns.1.name: "hour" } + - match: { columns.2.name: "day_name" } + - match: { columns.3.name: "month_name" } + + - length: { values: 4 } + - match: { values.0.0: "2025-05-31T00:00:00.000Z" } + - match: { values.0.1: 4 } + - match: { values.0.2: "Saturday" } + - match: { values.0.3: "May" } + - match: { values.1.0: "2025-05-31T01:00:00.000Z" } + - match: { values.1.1: 5 } + - match: { values.1.2: "Saturday" } + - match: { values.1.3: "May" } + - match: { values.2.0: "2025-05-31T22:00:00.000Z" } + - match: { values.2.1: 2 } + - match: { values.2.2: "Sunday" } + - match: { values.2.3: "June" } + - match: { values.3.0: "2025-05-31T23:00:00.000Z" } + - match: { values.3.1: 3 } + - match: { values.3.2: "Sunday" } + - match: { values.3.3: "June" } From 569d9e53081655dbb9627107105ef62e6fdd0967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 17 Oct 2025 14:44:58 +0200 Subject: [PATCH 02/54] Add SET CSV tests, and adapted CsvTests to work with SET statements --- .../xpack/esql/CsvSpecReader.java | 14 +------- .../xpack/esql/EsqlTestUtils.java | 11 ++++-- .../src/main/resources/set.csv-spec | 36 +++++++++++++++++++ .../xpack/esql/parser/EsqlParser.java | 5 +++ .../xpack/esql/session/Configuration.java | 2 +- .../elasticsearch/xpack/esql/CsvTests.java | 23 ++++++++---- 6 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugin/esql/qa/testFixtures/src/main/resources/set.csv-spec diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvSpecReader.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvSpecReader.java index ba0d11059a69b..0f315b0d12541 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvSpecReader.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvSpecReader.java @@ -13,9 +13,6 @@ import java.util.function.Function; import java.util.regex.Pattern; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - public final class CsvSpecReader { private CsvSpecReader() {} @@ -25,9 +22,6 @@ public static SpecReader.Parser specParser() { } public static class CsvSpecParser implements SpecReader.Parser { - private static final String SCHEMA_PREFIX = "schema::"; - - private final StringBuilder earlySchema = new StringBuilder(); private final StringBuilder query = new StringBuilder(); private final StringBuilder data = new StringBuilder(); private final List requiredCapabilities = new ArrayList<>(); @@ -39,10 +33,7 @@ private CsvSpecParser() {} public Object parse(String line) { // read the query if (testCase == null) { - if (line.startsWith(SCHEMA_PREFIX)) { - assertThat("Early schema already declared " + earlySchema, earlySchema.length(), is(0)); - earlySchema.append(line.substring(SCHEMA_PREFIX.length()).trim()); - } else if (line.toLowerCase(Locale.ROOT).startsWith("required_capability:")) { + if (line.toLowerCase(Locale.ROOT).startsWith("required_capability:")) { requiredCapabilities.add(line.substring("required_capability:".length()).trim()); } else { if (line.endsWith(";")) { @@ -50,10 +41,8 @@ public Object parse(String line) { testCase = new CsvTestCase(); query.append(line.substring(0, line.length() - 1).trim()); testCase.query = query.toString(); - testCase.earlySchema = earlySchema.toString(); testCase.requiredCapabilities = List.copyOf(requiredCapabilities); requiredCapabilities.clear(); - earlySchema.setLength(0); query.setLength(0); } // keep reading the query @@ -109,7 +98,6 @@ private static Pattern warningRegexToPattern(String regex) { public static class CsvTestCase { public String query; - public String earlySchema; public String expectedResults; private final List expectedWarnings = new ArrayList<>(); private final List expectedWarningsRegexString = new ArrayList<>(); diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java index 88469a8d19e8c..ee57a69ae021b 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java @@ -78,7 +78,6 @@ import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.core.type.EsField; -import org.elasticsearch.xpack.esql.core.util.DateUtils; import org.elasticsearch.xpack.esql.core.util.StringUtils; import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry; import org.elasticsearch.xpack.esql.expression.function.scalar.spatial.StGeohash; @@ -99,6 +98,8 @@ import org.elasticsearch.xpack.esql.inference.InferenceService; import org.elasticsearch.xpack.esql.optimizer.LogicalOptimizerContext; import org.elasticsearch.xpack.esql.parser.QueryParam; +import org.elasticsearch.xpack.esql.plan.EsqlStatement; +import org.elasticsearch.xpack.esql.plan.QuerySettings; import org.elasticsearch.xpack.esql.plan.logical.Enrich; import org.elasticsearch.xpack.esql.plan.logical.EsRelation; import org.elasticsearch.xpack.esql.plan.logical.Limit; @@ -516,9 +517,9 @@ private static ThreadPool createMockThreadPool() { private EsqlTestUtils() {} - public static Configuration configuration(QueryPragmas pragmas, String query) { + public static Configuration configuration(QueryPragmas pragmas, String query, EsqlStatement statement) { return new Configuration( - DateUtils.UTC, + statement.setting(QuerySettings.TIME_ZONE), Locale.US, null, null, @@ -535,6 +536,10 @@ public static Configuration configuration(QueryPragmas pragmas, String query) { ); } + public static Configuration configuration(QueryPragmas pragmas, String query) { + return configuration(pragmas, query, new EsqlStatement(null, List.of())); + } + public static Configuration configuration(QueryPragmas pragmas) { return configuration(pragmas, StringUtils.EMPTY); } diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/set.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/set.csv-spec new file mode 100644 index 0000000000000..f1336ac6ff797 --- /dev/null +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/set.csv-spec @@ -0,0 +1,36 @@ +set +required_capability: global_timezone_parameter + +set time_zone="+02:00"; from employees +| sort emp_no +| keep emp_no, hire_date +| eval hour = date_extract("hour_of_day", hire_date) +| limit 1; + +emp_no:integer | hire_date:date | hour:long +10001 | 1986-06-26T00:00:00.000Z | 2 +; + +set with foldable +required_capability: global_timezone_parameter + +set time_zone="+02:00"; ROW date = "1986-06-26T00:00:00.000Z"::date +| eval hour = date_extract("hour_of_day", date) +; + +date:date | hour:long +1986-06-26T00:00:00.000Z | 2 +; + +last set prevails +required_capability: global_timezone_parameter + +set time_zone="+02:00"; set time_zone="+05:00"; from employees +| sort emp_no +| keep emp_no, hire_date +| eval hour = date_extract("hour_of_day", hire_date) +| limit 1; + +emp_no:integer | hire_date:date | hour:long +10001 | 1986-06-26T00:00:00.000Z | 5 +; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlParser.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlParser.java index 1c844d3c15f6c..0c0a6a18c9d11 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlParser.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/EsqlParser.java @@ -115,6 +115,11 @@ public LogicalPlan createStatement(String query, QueryParams params, PlanTelemet return invokeParser(query, params, metrics, EsqlBaseParser::singleStatement, AstBuilder::plan); } + // testing utility + public EsqlStatement createQuery(String query) { + return createQuery(query, new QueryParams()); + } + // testing utility public EsqlStatement createQuery(String query, QueryParams params) { return createQuery(query, params, new PlanTelemetry(new EsqlFunctionRegistry())); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/Configuration.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/Configuration.java index 7e2dea975f326..9d248ffc19abe 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/Configuration.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/Configuration.java @@ -202,7 +202,7 @@ public long absoluteStartedTimeInMillis() { /** * @return Start time of the ESQL query in nanos */ - public long getQueryStartTimeNanos() { + public long queryStartTimeNanos() { return queryStartTimeNanos; } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java index f90547f57c0ff..99eefe0ba886d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java @@ -79,6 +79,7 @@ import org.elasticsearch.xpack.esql.optimizer.LogicalPreOptimizerContext; import org.elasticsearch.xpack.esql.optimizer.TestLocalPhysicalPlanOptimizer; import org.elasticsearch.xpack.esql.parser.EsqlParser; +import org.elasticsearch.xpack.esql.plan.EsqlStatement; import org.elasticsearch.xpack.esql.plan.logical.Enrich; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.esql.plan.physical.ChangePointExec; @@ -189,9 +190,13 @@ public class CsvTests extends ESTestCase { private final CsvSpecReader.CsvTestCase testCase; private final String instructions; - private final Configuration configuration = EsqlTestUtils.configuration( - new QueryPragmas(Settings.builder().put("page_size", randomPageSize()).build()) - ); + /** + * The configuration to be used in the tests. + *

+ * Initialized in {@link #executePlan}. + *

+ */ + private Configuration configuration; private final EsqlFunctionRegistry functionRegistry = new EsqlFunctionRegistry(); private final EsqlParser parser = new EsqlParser(); private final Mapper mapper = new Mapper(); @@ -526,6 +531,7 @@ private static EnrichPolicy loadEnrichPolicyMapping(String policyFileName) { private LogicalPlan analyzedPlan( LogicalPlan parsed, + Configuration configuration, CsvTestsDataLoader.MultiIndexTestDataset datasets, TransportVersion minimumVersion ) { @@ -594,11 +600,16 @@ private static TestPhysicalOperationProviders testOperationProviders( } private ActualResults executePlan(BigArrays bigArrays) throws Exception { - LogicalPlan parsed = parser.createStatement(testCase.query); - var testDatasets = testDatasets(parsed); + EsqlStatement statement = parser.createQuery(testCase.query); + this.configuration = EsqlTestUtils.configuration( + new QueryPragmas(Settings.builder().put("page_size", randomPageSize()).build()), + testCase.query, + statement + ); + var testDatasets = testDatasets(statement.plan()); // Specifically use the newest transport version; the csv tests correspond to a single node cluster on the current version. TransportVersion minimumVersion = TransportVersion.current(); - LogicalPlan analyzed = analyzedPlan(parsed, testDatasets, minimumVersion); + LogicalPlan analyzed = analyzedPlan(statement.plan(), configuration, testDatasets, minimumVersion); FoldContext foldCtx = FoldContext.small(); EsqlSession session = new EsqlSession( From cc15c9fa9590e5cff11a6bb903b300dee71c6c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 17 Oct 2025 14:52:55 +0200 Subject: [PATCH 03/54] Escape ; in csv tests with backslash --- .../org/elasticsearch/xpack/esql/CsvSpecReader.java | 8 +++++++- .../qa/testFixtures/src/main/resources/set.csv-spec | 10 +++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvSpecReader.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvSpecReader.java index 0f315b0d12541..0af1ac662fa69 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvSpecReader.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvSpecReader.java @@ -36,7 +36,13 @@ public Object parse(String line) { if (line.toLowerCase(Locale.ROOT).startsWith("required_capability:")) { requiredCapabilities.add(line.substring("required_capability:".length()).trim()); } else { - if (line.endsWith(";")) { + if (line.endsWith("\\;")) { + // SET statement with escaped ";" + var updatedLine = line.substring(0, line.length() - 2); + query.append(updatedLine); + query.append(";"); + query.append("\r\n"); + } else if (line.endsWith(";")) { // pick up the query testCase = new CsvTestCase(); query.append(line.substring(0, line.length() - 1).trim()); diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/set.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/set.csv-spec index f1336ac6ff797..24702a83ce79f 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/set.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/set.csv-spec @@ -1,7 +1,8 @@ set required_capability: global_timezone_parameter -set time_zone="+02:00"; from employees +set time_zone="+02:00"\; +from employees | sort emp_no | keep emp_no, hire_date | eval hour = date_extract("hour_of_day", hire_date) @@ -14,7 +15,8 @@ emp_no:integer | hire_date:date | hour:long set with foldable required_capability: global_timezone_parameter -set time_zone="+02:00"; ROW date = "1986-06-26T00:00:00.000Z"::date +set time_zone="+02:00"\; +ROW date = "1986-06-26T00:00:00.000Z"::date | eval hour = date_extract("hour_of_day", date) ; @@ -25,7 +27,9 @@ date:date | hour:long last set prevails required_capability: global_timezone_parameter -set time_zone="+02:00"; set time_zone="+05:00"; from employees +set time_zone="+02:00"\; +set time_zone="+05:00"\; +from employees | sort emp_no | keep emp_no, hire_date | eval hour = date_extract("hour_of_day", hire_date) From 449abe68e457f6f7b9d096fba088038d257b8353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 17 Oct 2025 18:12:51 +0200 Subject: [PATCH 04/54] Randomize configuration in function tests, and make it static for the ones that use it --- .../xpack/esql/EsqlTestUtils.java | 2 +- .../function/AbstractAggregationTestCase.java | 2 + .../function/AbstractFunctionTestCase.java | 4 ++ .../expression/function/TestCaseSupplier.java | 55 ++++++++++++++++++- ...AbstractConfigurationFunctionTestCase.java | 29 ++-------- .../scalar/date/DateExtractTests.java | 9 +-- .../function/scalar/date/DateFormatTests.java | 9 +++ .../function/scalar/date/DayNameTests.java | 7 ++- .../function/scalar/date/MonthNameTests.java | 6 +- .../function/scalar/date/NowTests.java | 2 +- .../function/scalar/string/ToLowerTests.java | 4 +- .../function/scalar/string/ToUpperTests.java | 4 +- 12 files changed, 90 insertions(+), 43 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java index ee57a69ae021b..9837c7232c1ab 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java @@ -545,7 +545,7 @@ public static Configuration configuration(QueryPragmas pragmas) { } public static Configuration configuration(String query) { - return configuration(new QueryPragmas(Settings.EMPTY), query); + return configuration(QueryPragmas.EMPTY, query); } public static AnalyzerSettings queryClusterSettings() { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractAggregationTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractAggregationTestCase.java index 69b6ff4ff30d1..7805a8e603334 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractAggregationTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractAggregationTestCase.java @@ -117,6 +117,8 @@ protected static List withNoRowsExpectingNull(List td.isMultiRow() ? td.withData(List.of()) : td).toList(); return new TestCaseSupplier.TestCase( + testCase.getSource(), + testCase.getConfiguration(), newData, testCase.evaluatorToString(), testCase.expectedType(), diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java index 61de9ce7601f9..8a60cbf756e3d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java @@ -179,6 +179,8 @@ protected static List anyNullIsNull( }).toList(); TestCaseSupplier.TypedData nulledData = oc.getData().get(finalNullPosition); return new TestCaseSupplier.TestCase( + oc.getSource(), + oc.getConfiguration(), data, evaluatorToString.evaluatorToString(finalNullPosition, nulledData, oc.evaluatorToString()), expectedType.expectedType(finalNullPosition, nulledData.type(), oc), @@ -211,6 +213,8 @@ protected static List anyNullIsNull( ) .toList(); return new TestCaseSupplier.TestCase( + oc.getSource(), + oc.getConfiguration(), data, equalTo("LiteralsEvaluator[lit=null]"), expectedType.expectedType(finalNullPosition, DataType.NULL, oc), diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java index 3d75db67e7604..e00a701ffe463 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java @@ -23,6 +23,7 @@ import org.elasticsearch.logging.Logger; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.esql.ConfigurationTestUtils; import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.Literal; @@ -42,8 +43,10 @@ import java.time.Period; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.function.BiFunction; import java.util.function.BinaryOperator; import java.util.function.DoubleFunction; @@ -1570,6 +1573,17 @@ public static String castToDoubleEvaluator(String original, DataType current) { throw new UnsupportedOperationException(); } + public static List mapTestCases( + Collection suppliers, + Function mapper + ) { + return suppliers.stream().map(supplier -> + new TestCaseSupplier(supplier.name(), supplier.types(), () -> + mapper.apply(supplier.get()) + ) + ).toList(); + } + public static final class TestCase { /** * The {@link Source} this test case should be run with @@ -1655,6 +1669,8 @@ public static TestCase typeError(List data, String expectedTypeError) Object extra ) { this( + TEST_SOURCE, + ConfigurationTestUtils.randomConfiguration(TEST_SOURCE.text(), Map.of()), data, evaluatorToString, expectedType, @@ -1670,6 +1686,8 @@ public static TestCase typeError(List data, String expectedTypeError) } TestCase( + Source source, + Configuration configuration, List data, Matcher evaluatorToString, DataType expectedType, @@ -1682,8 +1700,8 @@ public static TestCase typeError(List data, String expectedTypeError) Object extra, boolean canBuildEvaluator ) { - this.source = TEST_SOURCE; - this.configuration = TEST_CONFIGURATION; + this.source = source; + this.configuration = configuration; this.data = data; this.evaluatorToString = evaluatorToString; this.expectedType = expectedType == null ? null : expectedType.noText(); @@ -1779,11 +1797,34 @@ public Object extra() { return extra; } + /** + * Build a new {@link TestCase} with new {@link #configuration}. + */ + public TestCase withConfiguration(Configuration configuration) { + return new TestCase( + source, + configuration, + data, + evaluatorToString, + expectedType, + matcher, + expectedWarnings, + expectedBuildEvaluatorWarnings, + expectedTypeError, + foldingExceptionClass, + foldingExceptionMessage, + extra, + canBuildEvaluator + ); + } + /** * Build a new {@link TestCase} with new {@link #data}. */ public TestCase withData(List data) { return new TestCase( + source, + configuration, data, evaluatorToString, expectedType, @@ -1803,6 +1844,8 @@ public TestCase withData(List data) { */ public TestCase withExtra(Object extra) { return new TestCase( + source, + configuration, data, evaluatorToString, expectedType, @@ -1819,6 +1862,8 @@ public TestCase withExtra(Object extra) { public TestCase withWarning(String warning) { return new TestCase( + source, + configuration, data, evaluatorToString, expectedType, @@ -1839,6 +1884,8 @@ public TestCase withWarning(String warning) { */ public TestCase withBuildEvaluatorWarning(String warning) { return new TestCase( + source, + configuration, data, evaluatorToString, expectedType, @@ -1864,6 +1911,8 @@ private String[] addWarning(String[] warnings, String warning) { public TestCase withFoldingException(Class clazz, String message) { return new TestCase( + source, + configuration, data, evaluatorToString, expectedType, @@ -1886,6 +1935,8 @@ public TestCase withFoldingException(Class clazz, String me */ public TestCase withoutEvaluator() { return new TestCase( + source, + configuration, data, evaluatorToString, expectedType, diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java index 56e2197ce52ec..040ebe8f44004 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java @@ -7,18 +7,17 @@ package org.elasticsearch.xpack.esql.expression.function.scalar; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.xpack.esql.analysis.AnalyzerSettings; +import org.elasticsearch.xpack.esql.ConfigurationTestUtils; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; -import org.elasticsearch.xpack.esql.core.util.StringUtils; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; -import org.elasticsearch.xpack.esql.plugin.QueryPragmas; import org.elasticsearch.xpack.esql.session.Configuration; import java.util.List; import java.util.Map; +import static org.elasticsearch.xpack.esql.ConfigurationTestUtils.randomConfiguration; +import static org.elasticsearch.xpack.esql.ConfigurationTestUtils.randomTables; import static org.elasticsearch.xpack.esql.SerializationTestUtils.assertSerialization; public abstract class AbstractConfigurationFunctionTestCase extends AbstractScalarFunctionTestCase { @@ -35,29 +34,9 @@ public void testSerializationWithConfiguration() { assertSerialization(expr, config); - Configuration differentConfig = randomValueOtherThan(config, AbstractConfigurationFunctionTestCase::randomConfiguration); + Configuration differentConfig = randomValueOtherThan(config, () -> randomConfiguration(testCase.getSource().text(), randomTables())); Expression differentExpr = buildWithConfiguration(testCase.getSource(), testCase.getDataAsFields(), differentConfig); assertNotEquals(expr, differentExpr); } - - private static Configuration randomConfiguration() { - // TODO: Randomize the query and maybe the pragmas. - return new Configuration( - randomZone(), - randomLocale(random()), - randomBoolean() ? null : randomAlphaOfLength(randomInt(64)), - randomBoolean() ? null : randomAlphaOfLength(randomInt(64)), - QueryPragmas.EMPTY, - AnalyzerSettings.QUERY_RESULT_TRUNCATION_MAX_SIZE.getDefault(Settings.EMPTY), - AnalyzerSettings.QUERY_RESULT_TRUNCATION_DEFAULT_SIZE.getDefault(Settings.EMPTY), - StringUtils.EMPTY, - randomBoolean(), - Map.of(), - System.nanoTime(), - randomBoolean(), - AnalyzerSettings.QUERY_TIMESERIES_RESULT_TRUNCATION_MAX_SIZE.getDefault(Settings.EMPTY), - AnalyzerSettings.QUERY_TIMESERIES_RESULT_TRUNCATION_DEFAULT_SIZE.getDefault(Settings.EMPTY) - ); - } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java index 0067b022755e3..522fa91b3f364 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java @@ -56,7 +56,7 @@ public static Iterable parameters() { "DateExtractMillisEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", DataType.LONG, equalTo(2023L) - ) + ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) ), new TestCaseSupplier( List.of(stringType, DataType.DATE_NANOS), @@ -68,7 +68,7 @@ public static Iterable parameters() { "DateExtractNanosEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", DataType.LONG, equalTo(2023L) - ) + ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) ), new TestCaseSupplier( List.of(stringType, DataType.DATE_NANOS), @@ -80,7 +80,7 @@ public static Iterable parameters() { "DateExtractNanosEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", DataType.LONG, equalTo(123456L) - ) + ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) ), new TestCaseSupplier( List.of(stringType, DataType.DATETIME), @@ -93,7 +93,8 @@ public static Iterable parameters() { "DateExtractMillisEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", DataType.LONG, is(nullValue()) - ).withWarning("Line 1:1: evaluation of [source] failed, treating result as null. Only first 20 failures recorded.") + ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + .withWarning("Line 1:1: evaluation of [source] failed, treating result as null. Only first 20 failures recorded.") .withWarning( "Line 1:1: java.lang.IllegalArgumentException: " + "No enum constant java.time.temporal.ChronoField.NOT A UNIT" diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java index 1167b91a81e35..c7b2093f5cb63 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java @@ -13,6 +13,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateUtils; +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; @@ -23,9 +24,13 @@ import java.time.Instant; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; +import static java.util.stream.Collectors.toList; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.matchesPattern; public class DateFormatTests extends AbstractConfigurationFunctionTestCase { @@ -84,6 +89,10 @@ public static Iterable parameters() { (value) -> new BytesRef(EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER.formatNanos(DateUtils.toLong((Instant) value))), List.of() ); + suppliers = TestCaseSupplier.mapTestCases( + suppliers, + testCase -> testCase.withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ); return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors(true, suppliers); } 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 index 6b8b392f9d3f6..a45581df88254 100644 --- 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 @@ -14,6 +14,7 @@ import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.time.DateUtils; +import org.elasticsearch.xpack.esql.ConfigurationTestUtils; import org.elasticsearch.xpack.esql.analysis.AnalyzerSettings; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FoldContext; @@ -62,7 +63,7 @@ public static Iterable parameters() { Matchers.startsWith("DayNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(null) - ) + ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) ) ); @@ -78,7 +79,7 @@ private static List generateTest(String dateTime, String expec Matchers.startsWith("DayNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(new BytesRef(expectedWeekDay)) - ) + ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) ), new TestCaseSupplier( List.of(DataType.DATE_NANOS), @@ -87,7 +88,7 @@ private static List generateTest(String dateTime, String expec Matchers.is("DayNameNanosEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(new BytesRef(expectedWeekDay)) - ) + ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) ) ); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java index 13e91bf89627e..6f9bc3b577194 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java @@ -72,7 +72,7 @@ public static Iterable parameters() { Matchers.startsWith("MonthNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(null) - ) + ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) ) ); @@ -88,7 +88,7 @@ private static List generateTest(String dateTime, String expec Matchers.startsWith("MonthNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(new BytesRef(expectedMonthName)) - ) + ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) ), new TestCaseSupplier( List.of(DataType.DATE_NANOS), @@ -97,7 +97,7 @@ private static List generateTest(String dateTime, String expec Matchers.is("MonthNameNanosEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(new BytesRef(expectedMonthName)) - ) + ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) ) ); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java index b3c3ab874f90d..ab451d431fa2e 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java @@ -42,7 +42,7 @@ public static Iterable parameters() { matchesPattern("LiteralsEvaluator\\[lit=.*]"), DataType.DATETIME, equalTo(TestCaseSupplier.TEST_CONFIGURATION.now().toInstant().toEpochMilli()) - ) + ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) ) ) ); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java index ebc9c0883acf5..01e53de4087eb 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java @@ -91,7 +91,7 @@ private static void suppliers(List suppliers, String name, Dat values.add(new TestCaseSupplier.TypedData(new BytesRef(value), type, "0")); String expectedValue = value.toLowerCase(EsqlTestUtils.TEST_CFG.locale()); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION); })); suppliers.add(new TestCaseSupplier(name + " mv", List.of(type), () -> { List values = new ArrayList<>(); @@ -101,7 +101,7 @@ private static void suppliers(List suppliers, String name, Dat values.add(new TestCaseSupplier.TypedData(strings.stream().map(BytesRef::new).toList(), type, "0")); List expectedValue = strings.stream().map(s -> new BytesRef(s.toLowerCase(EsqlTestUtils.TEST_CFG.locale()))).toList(); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION); })); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java index 539e51420faf3..5357e43a0304b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java @@ -91,7 +91,7 @@ private static void supplier(List suppliers, String name, Data values.add(new TestCaseSupplier.TypedData(new BytesRef(value), type, "0")); String expectedValue = value.toUpperCase(EsqlTestUtils.TEST_CFG.locale()); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION); })); suppliers.add(new TestCaseSupplier(name + " mv", List.of(type), () -> { List values = new ArrayList<>(); @@ -101,7 +101,7 @@ private static void supplier(List suppliers, String name, Data values.add(new TestCaseSupplier.TypedData(strings.stream().map(BytesRef::new).toList(), type, "0")); List expectedValue = strings.stream().map(s -> new BytesRef(s.toUpperCase(EsqlTestUtils.TEST_CFG.locale()))).toList(); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION); })); } From 5dec7c3d9041eaf41dee56f8c59e3b47a038ac73 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 17 Oct 2025 16:21:21 +0000 Subject: [PATCH 05/54] [CI] Auto commit changes from spotless --- .../esql/expression/function/TestCaseSupplier.java | 8 +++----- .../scalar/AbstractConfigurationFunctionTestCase.java | 7 ++++--- .../function/scalar/date/DateExtractTests.java | 4 +++- .../function/scalar/date/DateFormatTests.java | 10 +--------- .../expression/function/scalar/date/DayNameTests.java | 1 - .../function/scalar/string/ToLowerTests.java | 8 ++++++-- .../function/scalar/string/ToUpperTests.java | 8 ++++++-- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java index e00a701ffe463..16aaf3e5ed26e 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java @@ -1577,11 +1577,9 @@ public static List mapTestCases( Collection suppliers, Function mapper ) { - return suppliers.stream().map(supplier -> - new TestCaseSupplier(supplier.name(), supplier.types(), () -> - mapper.apply(supplier.get()) - ) - ).toList(); + return suppliers.stream() + .map(supplier -> new TestCaseSupplier(supplier.name(), supplier.types(), () -> mapper.apply(supplier.get()))) + .toList(); } public static final class TestCase { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java index 040ebe8f44004..d1f98082fbb4b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java @@ -7,14 +7,12 @@ package org.elasticsearch.xpack.esql.expression.function.scalar; -import org.elasticsearch.xpack.esql.ConfigurationTestUtils; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; import org.elasticsearch.xpack.esql.session.Configuration; import java.util.List; -import java.util.Map; import static org.elasticsearch.xpack.esql.ConfigurationTestUtils.randomConfiguration; import static org.elasticsearch.xpack.esql.ConfigurationTestUtils.randomTables; @@ -34,7 +32,10 @@ public void testSerializationWithConfiguration() { assertSerialization(expr, config); - Configuration differentConfig = randomValueOtherThan(config, () -> randomConfiguration(testCase.getSource().text(), randomTables())); + Configuration differentConfig = randomValueOtherThan( + config, + () -> randomConfiguration(testCase.getSource().text(), randomTables()) + ); Expression differentExpr = buildWithConfiguration(testCase.getSource(), testCase.getDataAsFields(), differentConfig); assertNotEquals(expr, differentExpr); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java index 522fa91b3f364..f840d214c1bfd 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java @@ -94,7 +94,9 @@ public static Iterable parameters() { DataType.LONG, is(nullValue()) ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) - .withWarning("Line 1:1: evaluation of [source] failed, treating result as null. Only first 20 failures recorded.") + .withWarning( + "Line 1:1: evaluation of [source] failed, treating result as null. Only first 20 failures recorded." + ) .withWarning( "Line 1:1: java.lang.IllegalArgumentException: " + "No enum constant java.time.temporal.ChronoField.NOT A UNIT" diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java index c7b2093f5cb63..ef69daf268502 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java @@ -13,7 +13,6 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateUtils; -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; @@ -24,13 +23,9 @@ import java.time.Instant; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.function.Function; import java.util.function.Supplier; -import static java.util.stream.Collectors.toList; -import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.matchesPattern; public class DateFormatTests extends AbstractConfigurationFunctionTestCase { @@ -89,10 +84,7 @@ public static Iterable parameters() { (value) -> new BytesRef(EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER.formatNanos(DateUtils.toLong((Instant) value))), List.of() ); - suppliers = TestCaseSupplier.mapTestCases( - suppliers, - testCase -> testCase.withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) - ); + suppliers = TestCaseSupplier.mapTestCases(suppliers, testCase -> testCase.withConfiguration(TestCaseSupplier.TEST_CONFIGURATION)); return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors(true, suppliers); } 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 index a45581df88254..f04fd3bf2c7b7 100644 --- 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 @@ -14,7 +14,6 @@ import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.time.DateUtils; -import org.elasticsearch.xpack.esql.ConfigurationTestUtils; import org.elasticsearch.xpack.esql.analysis.AnalyzerSettings; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FoldContext; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java index 01e53de4087eb..34910eba84c29 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java @@ -91,7 +91,9 @@ private static void suppliers(List suppliers, String name, Dat values.add(new TestCaseSupplier.TypedData(new BytesRef(value), type, "0")); String expectedValue = value.toLowerCase(EsqlTestUtils.TEST_CFG.locale()); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))).withConfiguration( + TestCaseSupplier.TEST_CONFIGURATION + ); })); suppliers.add(new TestCaseSupplier(name + " mv", List.of(type), () -> { List values = new ArrayList<>(); @@ -101,7 +103,9 @@ private static void suppliers(List suppliers, String name, Dat values.add(new TestCaseSupplier.TypedData(strings.stream().map(BytesRef::new).toList(), type, "0")); List expectedValue = strings.stream().map(s -> new BytesRef(s.toLowerCase(EsqlTestUtils.TEST_CFG.locale()))).toList(); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withConfiguration( + TestCaseSupplier.TEST_CONFIGURATION + ); })); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java index 5357e43a0304b..c9f8b5ab2791b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java @@ -91,7 +91,9 @@ private static void supplier(List suppliers, String name, Data values.add(new TestCaseSupplier.TypedData(new BytesRef(value), type, "0")); String expectedValue = value.toUpperCase(EsqlTestUtils.TEST_CFG.locale()); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))).withConfiguration( + TestCaseSupplier.TEST_CONFIGURATION + ); })); suppliers.add(new TestCaseSupplier(name + " mv", List.of(type), () -> { List values = new ArrayList<>(); @@ -101,7 +103,9 @@ private static void supplier(List suppliers, String name, Data values.add(new TestCaseSupplier.TypedData(strings.stream().map(BytesRef::new).toList(), type, "0")); List expectedValue = strings.stream().map(s -> new BytesRef(s.toUpperCase(EsqlTestUtils.TEST_CFG.locale()))).toList(); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withConfiguration( + TestCaseSupplier.TEST_CONFIGURATION + ); })); } From b088abf8f42a43439ae01f53043bd6dae2709d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Mon, 27 Oct 2025 13:01:31 +0100 Subject: [PATCH 06/54] Moved custom config cases to another method --- .../esql/expression/function/TestCaseSupplier.java | 10 ++++++++++ .../function/scalar/date/DateExtractTests.java | 8 ++++---- .../function/scalar/date/DateFormatTests.java | 2 +- .../expression/function/scalar/date/DayNameTests.java | 6 +++--- .../function/scalar/date/MonthNameTests.java | 6 +++--- .../esql/expression/function/scalar/date/NowTests.java | 2 +- .../function/scalar/string/ToLowerTests.java | 9 +++------ .../function/scalar/string/ToUpperTests.java | 9 +++------ 8 files changed, 28 insertions(+), 24 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java index 16aaf3e5ed26e..e8d7236f67674 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java @@ -1795,6 +1795,16 @@ public Object extra() { return extra; } + /** + * Build a new {@link TestCase} with the {@link #TEST_CONFIGURATION}. + * + * @deprecated Use a custom configuration instead, and test the results. + */ + @Deprecated + public TestCase withStaticConfiguration() { + return withConfiguration(TEST_CONFIGURATION); + } + /** * Build a new {@link TestCase} with new {@link #configuration}. */ diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java index f1578edd8bf4c..fb7ec0ddb3897 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java @@ -56,7 +56,7 @@ public static Iterable parameters() { "DateExtractMillisEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", DataType.LONG, equalTo(2023L) - ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ).withStaticConfiguration() ), new TestCaseSupplier( List.of(stringType, DataType.DATE_NANOS), @@ -68,7 +68,7 @@ public static Iterable parameters() { "DateExtractNanosEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", DataType.LONG, equalTo(2023L) - ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ).withStaticConfiguration() ), new TestCaseSupplier( List.of(stringType, DataType.DATE_NANOS), @@ -80,7 +80,7 @@ public static Iterable parameters() { "DateExtractNanosEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", DataType.LONG, equalTo(123456L) - ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ).withStaticConfiguration() ), new TestCaseSupplier( List.of(stringType, DataType.DATETIME), @@ -93,7 +93,7 @@ public static Iterable parameters() { "DateExtractMillisEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", DataType.LONG, is(nullValue()) - ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ).withStaticConfiguration() .withWarning( "Line 1:1: evaluation of [source] failed, treating result as null. Only first 20 failures recorded." ) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java index d5f0f1adad960..10ead11f09273 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java @@ -84,7 +84,7 @@ public static Iterable parameters() { (value) -> new BytesRef(EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER.formatNanos(DateUtils.toLong((Instant) value))), List.of() ); - suppliers = TestCaseSupplier.mapTestCases(suppliers, testCase -> testCase.withConfiguration(TestCaseSupplier.TEST_CONFIGURATION)); + suppliers = TestCaseSupplier.mapTestCases(suppliers, testCase -> testCase.withStaticConfiguration()); return parameterSuppliersFromTypedDataWithDefaultChecks(true, suppliers); } 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 index 971e34fd078fe..b50377660c8c5 100644 --- 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 @@ -62,7 +62,7 @@ public static Iterable parameters() { Matchers.startsWith("DayNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(null) - ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ).withStaticConfiguration() ) ); @@ -78,7 +78,7 @@ private static List generateTest(String dateTime, String expec Matchers.startsWith("DayNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(new BytesRef(expectedWeekDay)) - ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ).withStaticConfiguration() ), new TestCaseSupplier( List.of(DataType.DATE_NANOS), @@ -87,7 +87,7 @@ private static List generateTest(String dateTime, String expec Matchers.is("DayNameNanosEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(new BytesRef(expectedWeekDay)) - ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ).withStaticConfiguration() ) ); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java index f9aa63651077a..9c4e6c2007b12 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java @@ -72,7 +72,7 @@ public static Iterable parameters() { Matchers.startsWith("MonthNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(null) - ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ).withStaticConfiguration() ) ); @@ -88,7 +88,7 @@ private static List generateTest(String dateTime, String expec Matchers.startsWith("MonthNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(new BytesRef(expectedMonthName)) - ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ).withStaticConfiguration() ), new TestCaseSupplier( List.of(DataType.DATE_NANOS), @@ -97,7 +97,7 @@ private static List generateTest(String dateTime, String expec Matchers.is("MonthNameNanosEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), DataType.KEYWORD, equalTo(new BytesRef(expectedMonthName)) - ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ).withStaticConfiguration() ) ); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java index 976bcb78f9b97..ef5d6c2a18950 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java @@ -42,7 +42,7 @@ public static Iterable parameters() { matchesPattern("LiteralsEvaluator\\[lit=.*]"), DataType.DATETIME, equalTo(TestCaseSupplier.TEST_CONFIGURATION.now().toInstant().toEpochMilli()) - ).withConfiguration(TestCaseSupplier.TEST_CONFIGURATION) + ).withStaticConfiguration() ) ) ); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java index d6811811ece5b..894b1249426ec 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java @@ -91,9 +91,8 @@ private static void suppliers(List suppliers, String name, Dat values.add(new TestCaseSupplier.TypedData(new BytesRef(value), type, "0")); String expectedValue = value.toLowerCase(EsqlTestUtils.TEST_CFG.locale()); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))).withConfiguration( - TestCaseSupplier.TEST_CONFIGURATION - ); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))) + .withStaticConfiguration(); })); suppliers.add(new TestCaseSupplier(name + " mv", List.of(type), () -> { List values = new ArrayList<>(); @@ -103,9 +102,7 @@ private static void suppliers(List suppliers, String name, Dat values.add(new TestCaseSupplier.TypedData(strings.stream().map(BytesRef::new).toList(), type, "0")); List expectedValue = strings.stream().map(s -> new BytesRef(s.toLowerCase(EsqlTestUtils.TEST_CFG.locale()))).toList(); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withConfiguration( - TestCaseSupplier.TEST_CONFIGURATION - ); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withStaticConfiguration(); })); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java index 8325fd73e59aa..c3ab1ae0c2614 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java @@ -91,9 +91,8 @@ private static void supplier(List suppliers, String name, Data values.add(new TestCaseSupplier.TypedData(new BytesRef(value), type, "0")); String expectedValue = value.toUpperCase(EsqlTestUtils.TEST_CFG.locale()); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))).withConfiguration( - TestCaseSupplier.TEST_CONFIGURATION - ); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))) + .withStaticConfiguration(); })); suppliers.add(new TestCaseSupplier(name + " mv", List.of(type), () -> { List values = new ArrayList<>(); @@ -103,9 +102,7 @@ private static void supplier(List suppliers, String name, Data values.add(new TestCaseSupplier.TypedData(strings.stream().map(BytesRef::new).toList(), type, "0")); List expectedValue = strings.stream().map(s -> new BytesRef(s.toUpperCase(EsqlTestUtils.TEST_CFG.locale()))).toList(); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withConfiguration( - TestCaseSupplier.TEST_CONFIGURATION - ); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withStaticConfiguration(); })); } From ee1316caf406bac853c0df62c35e00a41e61c446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Mon, 27 Oct 2025 14:51:32 +0100 Subject: [PATCH 07/54] Add comment on function with config dserialization randomization --- .../function/scalar/AbstractConfigurationFunctionTestCase.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java index 950005fbcb1ca..6788be578e77e 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java @@ -36,6 +36,7 @@ public void testSerializationWithConfiguration() { Configuration differentConfig = randomValueOtherThan( config, + // The source must match the original (static) one, as function source serialization depends on it () -> randomConfiguration(testCase.getSource().text(), randomTables()) ); From 13d8b4b6120ae2b78cc99868c372df5aa95e98ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Mon, 27 Oct 2025 17:46:29 +0100 Subject: [PATCH 08/54] Fix multi cluster tests to accept SET statements --- .../xpack/esql/ccq/MultiClusterSpecIT.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java index edb579024da31..57bb347dfda2a 100644 --- a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java +++ b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java @@ -304,7 +304,15 @@ static CsvSpecReader.CsvTestCase convertToRemoteIndices(CsvSpecReader.CsvTestCas String first = commands[0].trim(); // If true, we're using *:index, otherwise we're using *:index,index boolean onlyRemotes = canUseRemoteIndicesOnly() && randomBoolean(); - String[] commandParts = first.split("\\s+", 2); + + // Split "SET a=b; FROM x" into "SET a=b" and "FROM x" + int lastSetDelimiterPosition = first.lastIndexOf(';'); + String setStatements = lastSetDelimiterPosition == -1 ? "" : first.substring(0, lastSetDelimiterPosition + 1); + String afterSetStatements = lastSetDelimiterPosition == -1 ? first : first.substring(lastSetDelimiterPosition + 1); + + // Split "FROM a, b, c" into "FROM" and "a, b, c" + String[] commandParts = afterSetStatements.trim().split("\\s+", 2); + String command = commandParts[0].trim(); if (command.equalsIgnoreCase("from") || command.equalsIgnoreCase("ts")) { String[] indexMetadataParts = commandParts[1].split("(?i)\\bmetadata\\b", 2); @@ -326,7 +334,7 @@ static CsvSpecReader.CsvTestCase convertToRemoteIndices(CsvSpecReader.CsvTestCas + remoteIndices + " " + (indexMetadataParts.length == 1 ? "" : "metadata " + indexMetadataParts[1]); - testCase.query = newFirstCommand + query.substring(first.length()); + testCase.query = setStatements + newFirstCommand + query.substring(first.length()); } } From 2713280219bfbdbbd2cc3d7349f5d422b77cbad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Tue, 28 Oct 2025 12:10:42 +0100 Subject: [PATCH 09/54] Fix configuration and source matching in random tests --- .../esql/expression/function/TestCaseSupplier.java | 10 ++++++++-- .../scalar/AbstractConfigurationFunctionTestCase.java | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java index e8d7236f67674..04c6811619ca0 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java @@ -1797,18 +1797,24 @@ public Object extra() { /** * Build a new {@link TestCase} with the {@link #TEST_CONFIGURATION}. + *

+ * The source is also set to match the configuration + *

* * @deprecated Use a custom configuration instead, and test the results. */ @Deprecated public TestCase withStaticConfiguration() { - return withConfiguration(TEST_CONFIGURATION); + return withConfiguration(TEST_SOURCE, TEST_CONFIGURATION); } /** * Build a new {@link TestCase} with new {@link #configuration}. + *

+ * As the configuration query should match the source, the source is also updated here. + *

*/ - public TestCase withConfiguration(Configuration configuration) { + public TestCase withConfiguration(Source source, Configuration configuration) { return new TestCase( source, configuration, diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java index 6788be578e77e..116e8ee34e942 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java @@ -29,7 +29,7 @@ protected Expression build(Source source, List args) { public void testSerializationWithConfiguration() { assumeTrue("can't serialize function", canSerialize()); - Configuration config = randomConfiguration(); + Configuration config = randomConfiguration(testCase.getSource().text(), randomTables()); Expression expr = buildWithConfiguration(testCase.getSource(), testCase.getDataAsFields(), config); assertSerialization(expr, config); From f76f5ec5c31c5ffec17f088b823dc40025faadf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Tue, 28 Oct 2025 12:51:57 +0100 Subject: [PATCH 10/54] Added function tests for timezones --- .../src/main/resources/date.csv-spec | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) 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 8beef707706ca..ca610fcc2deab 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 @@ -55,7 +55,6 @@ emp_no:integer | x:keyword | y:keyword 10061 | 1985-09-17T00:00:00.000Z | 1985-09-17 ; - compareToString from employees | where hire_date < "1985-03-01T00:00:00Z" | keep emp_no, hire_date; ignoreOrder:true @@ -1085,6 +1084,33 @@ date:date | year:long 2022-05-06T00:00:00.000Z | 2022 ; +dateExtractSetTimezoneFrom +required_capability: global_timezone_parameter + +set time_zone="+07:00"\; +from employees +| sort hire_date +| eval hour = date_extract("hour_of_day", hire_date) +| keep emp_no, hire_date, dat +| limit 2; + +emp_no:integer | hire_date:date | hour:long +10009 | 1985-02-18T00:00:00.000Z | 7 +10048 | 1985-02-24T00:00:00.000Z | 7 +; + +dateExtractSetTimezoneRow +required_capability: global_timezone_parameter + +set time_zone="+07:00"\; +ROW hire_date = "2020-02-28T23:00:00.000Z"::date +| EVAL hour = date_extract("hour_of_day", hire_date) +| KEEP hour; + +hour:long +6 +; + docsDateExtractBusinessHours // tag::docsDateExtractBusinessHours[] FROM sample_data @@ -1971,6 +1997,30 @@ emp_no:integer | birth_date:date | hire_date:date 10040 | null | 1993-02-14T00:00:00.000Z | null ; +dayNameSetTimezoneRow +required_capability: global_timezone_parameter + +set time_zone="-02:00"\; +row dt = to_datetime("1953-09-02T00:00:00.000Z") +| eval weekday = day_name(dt); + +dt:date | weekday:keyword +1953-09-02T00:00:00.000Z | Tuesday +; + +dayNameSetTimezoneFrom +required_capability: global_timezone_parameter + +set time_zone="-02:00"\; +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 | Wednesday +; monthNameRowTest required_capability:fn_month_name @@ -2068,3 +2118,28 @@ from employees emp_no:integer | birth_date:date | hire_date:date | monthName:keyword 10040 | null | 1993-02-14T00:00:00.000Z | null ; + +monthNameSetTimezoneRow +required_capability: global_timezone_parameter + +set time_zone="Europe/Paris"\; +row dt = to_datetime("1996-01-31T23:00:00.000Z") +| eval monthName = MONTH_NAME(dt); + +dt:date | monthName:keyword +1996-01-31T23:00:00.000Z | February +; + +monthNameSetTimezoneFrom +required_capability:fn_month_name + +set time_zone="-10:00"\; +from employees +| WHERE emp_no == 10004 +| keep emp_no, hire_date +| eval monthName = month_name(hire_date) +| limit 1; + +emp_no:integer | hire_date:date | monthName:keyword +10004 | 1986-12-01T00:00:00.000Z | November +; From 9005c7891c177fd3ab6da00fe2c1a607e8ec1b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Tue, 28 Oct 2025 12:58:10 +0100 Subject: [PATCH 11/54] Fix typo --- .../esql/qa/testFixtures/src/main/resources/date.csv-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ca610fcc2deab..55c2b98411593 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 @@ -1091,7 +1091,7 @@ set time_zone="+07:00"\; from employees | sort hire_date | eval hour = date_extract("hour_of_day", hire_date) -| keep emp_no, hire_date, dat +| keep emp_no, hire_date, hour | limit 2; emp_no:integer | hire_date:date | hour:long From fad189f52bf32374f77b45a8e9608844de5bfc3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Tue, 28 Oct 2025 13:21:54 +0100 Subject: [PATCH 12/54] Fixed capability requirement --- .../esql/qa/testFixtures/src/main/resources/date.csv-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 55c2b98411593..c3bf0d9d0ebbf 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 @@ -2131,7 +2131,7 @@ dt:date | monthName:keyword ; monthNameSetTimezoneFrom -required_capability:fn_month_name +required_capability: global_timezone_parameter set time_zone="-10:00"\; from employees From 4f62f231a83ea0900d42407c850f6d410b71b17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Tue, 28 Oct 2025 15:42:21 +0100 Subject: [PATCH 13/54] Fix ScalbTests --- .../function/scalar/math/ScalbTests.java | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java index b17d539a32d89..57d8546064f46 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java @@ -94,25 +94,26 @@ private static TestCaseSupplier test( Number res, Iterable warns ) { - var supplier = new TestCaseSupplier.TestCase( - List.of(new TestCaseSupplier.TypedData(d, dType, "d"), new TestCaseSupplier.TypedData(scaleFactor, scaleType, "scaleFactor")), - Strings.format( - "Scalb%sEvaluator[d=%s, scaleFactor=Attribute[channel=1]]", - pascalize(scaleType), - cast(dType) - - ), - DataType.DOUBLE, - equalTo(res) - ); - for (var warn : warns) { - supplier = supplier.withWarning(warn); - } - var finalSupplier = supplier; return new TestCaseSupplier( Strings.format("<%s>, <%s>", dType.typeName(), scaleType.typeName()), List.of(dType, scaleType), - () -> finalSupplier + () -> { + var supplier = new TestCaseSupplier.TestCase( + List.of(new TestCaseSupplier.TypedData(d, dType, "d"), new TestCaseSupplier.TypedData(scaleFactor, scaleType, "scaleFactor")), + Strings.format( + "Scalb%sEvaluator[d=%s, scaleFactor=Attribute[channel=1]]", + pascalize(scaleType), + cast(dType) + + ), + DataType.DOUBLE, + equalTo(res) + ); + for (var warn : warns) { + supplier = supplier.withWarning(warn); + } + return supplier; + } ); } From 4c5adc0bb56add9705e685daff1278b0fa1e5344 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 28 Oct 2025 14:48:41 +0000 Subject: [PATCH 14/54] [CI] Auto commit changes from spotless --- .../function/scalar/math/ScalbTests.java | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java index 57d8546064f46..1de02139f5cea 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/math/ScalbTests.java @@ -94,27 +94,26 @@ private static TestCaseSupplier test( Number res, Iterable warns ) { - return new TestCaseSupplier( - Strings.format("<%s>, <%s>", dType.typeName(), scaleType.typeName()), - List.of(dType, scaleType), - () -> { - var supplier = new TestCaseSupplier.TestCase( - List.of(new TestCaseSupplier.TypedData(d, dType, "d"), new TestCaseSupplier.TypedData(scaleFactor, scaleType, "scaleFactor")), - Strings.format( - "Scalb%sEvaluator[d=%s, scaleFactor=Attribute[channel=1]]", - pascalize(scaleType), - cast(dType) - - ), - DataType.DOUBLE, - equalTo(res) - ); - for (var warn : warns) { - supplier = supplier.withWarning(warn); - } - return supplier; + return new TestCaseSupplier(Strings.format("<%s>, <%s>", dType.typeName(), scaleType.typeName()), List.of(dType, scaleType), () -> { + var supplier = new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(d, dType, "d"), + new TestCaseSupplier.TypedData(scaleFactor, scaleType, "scaleFactor") + ), + Strings.format( + "Scalb%sEvaluator[d=%s, scaleFactor=Attribute[channel=1]]", + pascalize(scaleType), + cast(dType) + + ), + DataType.DOUBLE, + equalTo(res) + ); + for (var warn : warns) { + supplier = supplier.withWarning(warn); } - ); + return supplier; + }); } private static String cast(DataType from) { From 3f233bfc9f9d65ee47db3c571c3584d2eedf942d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Tue, 28 Oct 2025 18:24:50 +0100 Subject: [PATCH 15/54] Add configuration to DATE_TRUNC and related functions --- .../function/EsqlFunctionRegistry.java | 39 +++++++- .../expression/function/grouping/Bucket.java | 96 ++++++++++++------- .../expression/function/grouping/TBucket.java | 39 ++++++-- .../function/scalar/date/DateTrunc.java | 37 +++---- .../ReplaceDateTruncBucketWithRoundTo.java | 15 ++- .../xpack/esql/session/Configuration.java | 2 - .../grouping/BucketSerializationTests.java | 9 +- .../function/grouping/BucketTests.java | 9 +- .../function/grouping/TBucketTests.java | 9 +- .../scalar/date/DateTruncErrorTests.java | 3 +- .../scalar/date/DateTruncRoundingTests.java | 56 ++++++----- .../date/DateTruncSerializationTests.java | 4 +- .../function/scalar/date/DateTruncTests.java | 9 +- .../LocalLogicalPlanOptimizerTests.java | 9 +- .../rules/logical/FoldNullTests.java | 8 +- ...eplaceDateTruncBucketWithRoundToTests.java | 44 +++++---- ...TimeSeriesAggregateSerializationTests.java | 4 +- .../xpack/esql/planner/EvalMapperTests.java | 2 +- 18 files changed, 253 insertions(+), 141 deletions(-) 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 62013b8602a03..d9ae6b70c1e0f 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 @@ -344,7 +344,7 @@ private static FunctionDefinition[][] functions() { new FunctionDefinition[] { def(Bucket.class, Bucket::new, "bucket", "bin"), def(Categorize.class, Categorize::new, "categorize"), - def(TBucket.class, uni(TBucket::new), "tbucket") }, + def(TBucket.class, (UnaryConfigurationAwareBuilder) TBucket::new, "tbucket") }, // aggregate functions // since they declare two public constructors - one with filter (for nested where) and one without // use casting to disambiguate between the two @@ -1276,6 +1276,43 @@ protected interface TernaryConfigurationAwareBuilder { T build(Source source, Expression one, Expression two, Expression three, Configuration configuration); } + /** + * Build a {@linkplain FunctionDefinition} for a quaternary function that is configuration aware. + */ + @SuppressWarnings("overloads") // These are ambiguous if you aren't using ctor references but we always do + protected static FunctionDefinition def( + Class function, + QuaternaryConfigurationAwareBuilder ctorRef, + String... names + ) { + FunctionBuilder builder = (source, children, cfg) -> { + if (OptionalArgument.class.isAssignableFrom(function)) { + if (children.size() > 4 || children.size() < 3) { + throw new QlIllegalArgumentException("expects three or four arguments"); + } + } else if (TwoOptionalArguments.class.isAssignableFrom(function)) { + if (children.size() > 4 || children.size() < 2) { + throw new QlIllegalArgumentException("expects minimum two, maximum four arguments"); + } + } else if (children.size() != 4) { + throw new QlIllegalArgumentException("expects exactly four arguments"); + } + return ctorRef.build( + source, + children.get(0), + children.get(1), + children.size() > 2 ? children.get(2) : null, + children.size() > 3 ? children.get(3) : null, + cfg + ); + }; + return def(function, builder, names); + } + + protected interface QuaternaryConfigurationAwareBuilder { + T build(Source source, Expression one, Expression two, Expression three, Expression four, Configuration configuration); + } + // // Utility functions to help disambiguate the method handle passed in. // They work by providing additional method information to help the compiler know which method to pick. diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java index 1b94e1d2bb15a..11b6bdcfeeee0 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java @@ -35,10 +35,13 @@ import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Div; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mul; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; +import org.elasticsearch.xpack.esql.session.Configuration; import java.io.IOException; +import java.time.ZoneId; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import static org.elasticsearch.common.logging.LoggerMessageFormat.format; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; @@ -48,7 +51,6 @@ import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNumeric; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; import static org.elasticsearch.xpack.esql.expression.Validations.isFoldable; -import static org.elasticsearch.xpack.esql.session.Configuration.DEFAULT_TZ; import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToLong; /** @@ -65,27 +67,34 @@ public class Bucket extends GroupingFunction.EvaluatableGroupingFunction // TODO maybe we should just cover the whole of representable dates here - like ten years, 100 years, 1000 years, all the way up. // That way you never end up with more than the target number of buckets. - private static final Rounding LARGEST_HUMAN_DATE_ROUNDING = Rounding.builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY).build(); - private static final Rounding[] HUMAN_DATE_ROUNDINGS = new Rounding[] { - Rounding.builder(Rounding.DateTimeUnit.MONTH_OF_YEAR).build(), - Rounding.builder(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR).build(), - Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH).build(), - Rounding.builder(TimeValue.timeValueHours(12)).build(), - Rounding.builder(TimeValue.timeValueHours(3)).build(), - Rounding.builder(TimeValue.timeValueHours(1)).build(), - Rounding.builder(TimeValue.timeValueMinutes(30)).build(), - Rounding.builder(TimeValue.timeValueMinutes(10)).build(), - Rounding.builder(TimeValue.timeValueMinutes(5)).build(), - Rounding.builder(TimeValue.timeValueMinutes(1)).build(), - Rounding.builder(TimeValue.timeValueSeconds(30)).build(), - Rounding.builder(TimeValue.timeValueSeconds(10)).build(), - Rounding.builder(TimeValue.timeValueSeconds(5)).build(), - Rounding.builder(TimeValue.timeValueSeconds(1)).build(), - Rounding.builder(TimeValue.timeValueMillis(100)).build(), - Rounding.builder(TimeValue.timeValueMillis(50)).build(), - Rounding.builder(TimeValue.timeValueMillis(10)).build(), - Rounding.builder(TimeValue.timeValueMillis(1)).build(), }; - + private static final Rounding.Builder LARGEST_HUMAN_DATE_ROUNDING = Rounding.builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY); + private static final Rounding.Builder[] HUMAN_DATE_ROUNDINGS = new Rounding.Builder[] { + Rounding.builder(Rounding.DateTimeUnit.MONTH_OF_YEAR), + Rounding.builder(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR), + Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH), + Rounding.builder(TimeValue.timeValueHours(12)), + Rounding.builder(TimeValue.timeValueHours(3)), + Rounding.builder(TimeValue.timeValueHours(1)), + Rounding.builder(TimeValue.timeValueMinutes(30)), + Rounding.builder(TimeValue.timeValueMinutes(10)), + Rounding.builder(TimeValue.timeValueMinutes(5)), + Rounding.builder(TimeValue.timeValueMinutes(1)), + Rounding.builder(TimeValue.timeValueSeconds(30)), + Rounding.builder(TimeValue.timeValueSeconds(10)), + Rounding.builder(TimeValue.timeValueSeconds(5)), + Rounding.builder(TimeValue.timeValueSeconds(1)), + Rounding.builder(TimeValue.timeValueMillis(100)), + Rounding.builder(TimeValue.timeValueMillis(50)), + Rounding.builder(TimeValue.timeValueMillis(10)), + Rounding.builder(TimeValue.timeValueMillis(1)), }; + + /* + * As Bucket already extends GroupingFunction, it can't extend EsqlConfigurationFunction, so we had to replicate here: + * - The Configuration field + * - HashCode + * - Equals + */ + private final Configuration configuration; private final Expression field; private final Expression buckets; private final Expression from; @@ -208,13 +217,15 @@ public Bucket( type = { "integer", "long", "double", "date", "keyword", "text" }, optional = true, description = "End of the range. Can be a number, a date or a date expressed as a string." - ) Expression to + ) Expression to, + Configuration configuration ) { super(source, fields(field, buckets, from, to)); this.field = field; this.buckets = buckets; this.from = from; this.to = to; + this.configuration = configuration; } private Bucket(StreamInput in) throws IOException { @@ -223,10 +234,15 @@ private Bucket(StreamInput in) throws IOException { in.readNamedWriteable(Expression.class), in.readNamedWriteable(Expression.class), in.readOptionalNamedWriteable(Expression.class), - in.readOptionalNamedWriteable(Expression.class) + in.readOptionalNamedWriteable(Expression.class), + ((PlanStreamInput) in).configuration() ); } + public Configuration configuration() { + return configuration; + } + private static List fields(Expression field, Expression buckets, Expression from, Expression to) { List list = new ArrayList<>(4); list.add(field); @@ -308,19 +324,20 @@ public Rounding.Prepared getDateRounding(FoldContext foldContext, Long min, Long long f = foldToLong(foldContext, from); long t = foldToLong(foldContext, to); if (min != null && max != null) { - return new DateRoundingPicker(b, f, t).pickRounding().prepare(min, max); + return new DateRoundingPicker(b, f, t, configuration.zoneId()).pickRounding().prepare(min, max); } - return new DateRoundingPicker(b, f, t).pickRounding().prepareForUnknown(); + return new DateRoundingPicker(b, f, t, configuration.zoneId()).pickRounding().prepareForUnknown(); } else { assert DataType.isTemporalAmount(buckets.dataType()) : "Unexpected span data type [" + buckets.dataType() + "]"; - return DateTrunc.createRounding(buckets.fold(foldContext), DEFAULT_TZ, min, max); + return DateTrunc.createRounding(buckets.fold(foldContext), configuration.zoneId(), min, max); } } - private record DateRoundingPicker(int buckets, long from, long to) { + private record DateRoundingPicker(int buckets, long from, long to, ZoneId zoneId) { Rounding pickRounding() { - Rounding prev = LARGEST_HUMAN_DATE_ROUNDING; - for (Rounding r : HUMAN_DATE_ROUNDINGS) { + Rounding prev = LARGEST_HUMAN_DATE_ROUNDING.timeZone(zoneId).build(); + for (Rounding.Builder builder : HUMAN_DATE_ROUNDINGS) { + Rounding r = builder.timeZone(zoneId).build(); if (roundingIsOk(r)) { prev = r; } else { @@ -464,12 +481,12 @@ public DataType dataType() { public Expression replaceChildren(List newChildren) { Expression from = newChildren.size() > 2 ? newChildren.get(2) : null; Expression to = newChildren.size() > 3 ? newChildren.get(3) : null; - return new Bucket(source(), newChildren.get(0), newChildren.get(1), from, to); + return new Bucket(source(), newChildren.get(0), newChildren.get(1), from, to, configuration); } @Override protected NodeInfo info() { - return NodeInfo.create(this, Bucket::new, field, buckets, from, to); + return NodeInfo.create(this, Bucket::new, field, buckets, from, to, configuration); } public Expression field() { @@ -492,4 +509,19 @@ public Expression to() { public String toString() { return "Bucket{" + "field=" + field + ", buckets=" + buckets + ", from=" + from + ", to=" + to + '}'; } + + @Override + public int hashCode() { + return Objects.hash(getClass(), children(), configuration); + } + + @Override + public boolean equals(Object obj) { + if (super.equals(obj) == false) { + return false; + } + Bucket other = (Bucket) obj; + + return configuration.equals(other.configuration); + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucket.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucket.java index 5e2307459abd4..592f4c955ae7c 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucket.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucket.java @@ -19,9 +19,11 @@ import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.FunctionType; import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.session.Configuration; import java.io.IOException; import java.util.List; +import java.util.Objects; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; @@ -33,6 +35,13 @@ public class TBucket extends GroupingFunction.EvaluatableGroupingFunction implements SurrogateExpression { public static final String NAME = "TBucket"; + /* + * As Bucket already extends GroupingFunction, it can't extend EsqlConfigurationFunction, so we had to replicate here: + * - The Configuration field + * - HashCode + * - Equals + */ + private final Configuration configuration; private final Expression buckets; private final Expression timestamp; @@ -62,19 +71,22 @@ public class TBucket extends GroupingFunction.EvaluatableGroupingFunction implem ) public TBucket( Source source, - @Param(name = "buckets", type = { "date_period", "time_duration" }, description = "Desired bucket size.") Expression buckets + @Param(name = "buckets", type = { "date_period", "time_duration" }, description = "Desired bucket size.") Expression buckets, + Configuration configuration ) { this( source, buckets, - new UnresolvedTimestamp(source, "TBucket function requires @timestamp field, but @timestamp was renamed or dropped") + new UnresolvedTimestamp(source, "TBucket function requires @timestamp field, but @timestamp was renamed or dropped"), + configuration ); } - public TBucket(Source source, Expression buckets, Expression timestamp) { + public TBucket(Source source, Expression buckets, Expression timestamp, Configuration configuration) { super(source, List.of(buckets, timestamp)); this.buckets = buckets; this.timestamp = timestamp; + this.configuration = configuration; } @Override @@ -94,7 +106,7 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua @Override public Expression surrogate() { - return new Bucket(source(), timestamp, buckets, null, null); + return new Bucket(source(), timestamp, buckets, null, null, configuration); } @Override @@ -114,12 +126,12 @@ public DataType dataType() { @Override public Expression replaceChildren(List newChildren) { - return new TBucket(source(), newChildren.get(0), newChildren.get(1)); + return new TBucket(source(), newChildren.get(0), newChildren.get(1), configuration); } @Override protected NodeInfo info() { - return NodeInfo.create(this, TBucket::new, buckets, timestamp); + return NodeInfo.create(this, TBucket::new, buckets, timestamp, configuration); } public Expression field() { @@ -134,4 +146,19 @@ public Expression buckets() { public String toString() { return "TBucket{buckets=" + buckets + "}"; } + + @Override + public int hashCode() { + return Objects.hash(getClass(), children(), configuration); + } + + @Override + public boolean equals(Object obj) { + if (super.equals(obj) == false) { + return false; + } + TBucket other = (TBucket) obj; + + return configuration.equals(other.configuration); + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java index 401116cc6e419..48feb609b10e8 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java @@ -23,8 +23,9 @@ import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.Param; -import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction; +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.Duration; @@ -39,9 +40,8 @@ import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME; import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS; -import static org.elasticsearch.xpack.esql.session.Configuration.DEFAULT_TZ; -public class DateTrunc extends EsqlScalarFunction { +public class DateTrunc extends EsqlConfigurationFunction { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( Expression.class, "DateTrunc", @@ -83,15 +83,21 @@ public DateTrunc( type = { "date_period", "time_duration" }, description = "Interval; expressed using the timespan literal syntax." ) Expression interval, - @Param(name = "date", type = { "date", "date_nanos" }, description = "Date expression") Expression field + @Param(name = "date", type = { "date", "date_nanos" }, description = "Date expression") Expression field, + Configuration configuration ) { - super(source, List.of(interval, field)); + super(source, List.of(interval, field), configuration); this.interval = interval; this.timestampField = field; } private DateTrunc(StreamInput in) throws IOException { - this(Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class), in.readNamedWriteable(Expression.class)); + this( + Source.readFrom((PlanStreamInput) in), + in.readNamedWriteable(Expression.class), + in.readNamedWriteable(Expression.class), + ((PlanStreamInput) in).configuration() + ); } @Override @@ -144,12 +150,12 @@ static long processDateNanos(long fieldVal, @Fixed Rounding.Prepared rounding) { @Override public Expression replaceChildren(List newChildren) { - return new DateTrunc(source(), newChildren.get(0), newChildren.get(1)); + return new DateTrunc(source(), newChildren.get(0), newChildren.get(1), configuration()); } @Override protected NodeInfo info() { - return NodeInfo.create(this, DateTrunc::new, children().get(0), children().get(1)); + return NodeInfo.create(this, DateTrunc::new, children().get(0), children().get(1), configuration()); } @Override @@ -157,19 +163,6 @@ public boolean foldable() { return interval.foldable() && timestampField.foldable(); } - static Rounding.Prepared createRounding(final Object interval) { - return createRounding(interval, DEFAULT_TZ); - } - - public static Rounding.Prepared createRounding(final Object interval, final ZoneId timeZone) { - if (interval instanceof Period period) { - return createRounding(period, timeZone, null, null); - } else if (interval instanceof Duration duration) { - return createRounding(duration, timeZone, null, null); - } - throw new IllegalArgumentException("Time interval is not supported"); - } - public static Rounding.Prepared createRounding(final Object interval, final ZoneId timeZone, Long min, Long max) { if (interval instanceof Period period) { return createRounding(period, timeZone, min, max); @@ -259,7 +252,7 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { "Function [" + sourceText() + "] has invalid interval [" + interval.sourceText() + "]. " + e.getMessage() ); } - return evaluator(dataType(), source(), fieldEvaluator, DateTrunc.createRounding(foldedInterval, DEFAULT_TZ)); + return evaluator(dataType(), source(), fieldEvaluator, createRounding(foldedInterval, configuration().zoneId(), null, null)); } public static ExpressionEvaluator.Factory evaluator( diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundTo.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundTo.java index dd15ce180d577..f6db69ab8b142 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundTo.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundTo.java @@ -45,7 +45,6 @@ import java.util.stream.Collectors; import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTime; -import static org.elasticsearch.xpack.esql.session.Configuration.DEFAULT_TZ; import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateWithTypeToString; public class ReplaceDateTruncBucketWithRoundTo extends ParameterizedRule { @@ -54,34 +53,34 @@ public class ReplaceDateTruncBucketWithRoundTo extends ParameterizedRule substitute(eval, context.searchStats())) : plan; + return context.searchStats() != null ? plan.transformUp(Eval.class, eval -> substitute(eval, context)) : plan; } - private LogicalPlan substitute(Eval eval, SearchStats searchStats) { + private LogicalPlan substitute(Eval eval, LocalLogicalOptimizerContext context) { // check the filter in children plans - return eval.transformExpressionsOnly(Function.class, f -> substitute(f, eval, searchStats)); + return eval.transformExpressionsOnly(Function.class, f -> substitute(f, eval, context)); } /** * Perform the actual substitution with {@code SearchStats} and predicates in the query. */ - private Expression substitute(Expression e, Eval eval, SearchStats searchStats) { + private Expression substitute(Expression e, Eval eval, LocalLogicalOptimizerContext context) { Expression roundTo = null; if (e instanceof DateTrunc dateTrunc) { roundTo = maybeSubstituteWithRoundTo( dateTrunc.source(), dateTrunc.field(), dateTrunc.interval(), - searchStats, + context.searchStats(), eval, - (interval, minValue, maxValue) -> DateTrunc.createRounding(interval, DEFAULT_TZ, minValue, maxValue) + (interval, minValue, maxValue) -> DateTrunc.createRounding(interval, context.configuration().zoneId(), minValue, maxValue) ); } else if (e instanceof Bucket bucket) { roundTo = maybeSubstituteWithRoundTo( bucket.source(), bucket.field(), bucket.buckets(), - searchStats, + context.searchStats(), eval, (interval, minValue, maxValue) -> bucket.getDateRounding(FoldContext.small(), minValue, maxValue) ); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/Configuration.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/Configuration.java index 9d248ffc19abe..35260893790aa 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/Configuration.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/Configuration.java @@ -24,7 +24,6 @@ import java.time.Duration; import java.time.Instant; import java.time.ZoneId; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Locale; import java.util.Map; @@ -35,7 +34,6 @@ public class Configuration implements Writeable { public static final int QUERY_COMPRESS_THRESHOLD_CHARS = KB.toIntBytes(5); - public static final ZoneId DEFAULT_TZ = ZoneOffset.UTC; private static final TransportVersion TIMESERIES_DEFAULT_LIMIT = TransportVersion.fromName("timeseries_default_limit"); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketSerializationTests.java index 5fe270a4cce42..5cb7b91e8a191 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketSerializationTests.java @@ -10,22 +10,23 @@ import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.expression.AbstractExpressionSerializationTests; +import org.elasticsearch.xpack.esql.session.Configuration; import java.io.IOException; public class BucketSerializationTests extends AbstractExpressionSerializationTests { @Override protected Bucket createTestInstance() { - return createRandomBucket(); + return createRandomBucket(configuration()); } - public static Bucket createRandomBucket() { + public static Bucket createRandomBucket(Configuration configuration) { Source source = randomSource(); Expression field = randomChild(); Expression buckets = randomChild(); Expression from = randomChild(); Expression to = randomChild(); - return new Bucket(source, field, buckets, from, to); + return new Bucket(source, field, buckets, from, to, configuration); } @Override @@ -41,6 +42,6 @@ protected Bucket mutateInstance(Bucket instance) throws IOException { case 2 -> from = randomValueOtherThan(from, AbstractExpressionSerializationTests::randomChild); case 3 -> to = randomValueOtherThan(to, AbstractExpressionSerializationTests::randomChild); } - return new Bucket(source, field, buckets, from, to); + return new Bucket(source, field, buckets, from, to, configuration()); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java index 4d3c1cc2a7cdd..286759a5ed861 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java @@ -18,9 +18,10 @@ 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.AbstractScalarFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; +import org.elasticsearch.xpack.esql.session.Configuration; import org.hamcrest.Matcher; import org.hamcrest.Matchers; @@ -34,7 +35,7 @@ import static org.hamcrest.Matchers.equalTo; -public class BucketTests extends AbstractScalarFunctionTestCase { +public class BucketTests extends AbstractConfigurationFunctionTestCase { public BucketTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); } @@ -317,14 +318,14 @@ private static Matcher resultsMatcher(List t } @Override - protected Expression build(Source source, List args) { + protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { Expression from = null; Expression to = null; if (args.size() > 2) { from = args.get(2); to = args.get(3); } - return new Bucket(source, args.get(0), args.get(1), from, to); + return new Bucket(source, args.get(0), args.get(1), from, to, configuration); } /** diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java index d44dc58436adc..648f8f214ef39 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java @@ -17,9 +17,10 @@ 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.AbstractScalarFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.DocsV3Support; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; +import org.elasticsearch.xpack.esql.session.Configuration; import org.hamcrest.Matcher; import org.hamcrest.Matchers; @@ -35,7 +36,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; -public class TBucketTests extends AbstractScalarFunctionTestCase { +public class TBucketTests extends AbstractConfigurationFunctionTestCase { public TBucketTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); } @@ -138,8 +139,8 @@ private static Matcher resultsMatcher(List t } @Override - protected Expression build(Source source, List args) { - return new TBucket(source, args.get(0), args.get(1)); + protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { + return new TBucket(source, args.get(0), args.get(1), configuration); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncErrorTests.java index 9494888f9314f..e8e66b9651aee 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncErrorTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncErrorTests.java @@ -7,6 +7,7 @@ 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; @@ -27,7 +28,7 @@ protected List cases() { @Override protected Expression build(Source source, List args) { - return new DateTrunc(source, args.get(0), args.get(1)); + return new DateTrunc(source, args.get(0), args.get(1), EsqlTestUtils.TEST_CFG); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java index eab34bcfd98e4..6bedcd7334dc4 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java @@ -13,8 +13,8 @@ import java.time.Duration; import java.time.Instant; import java.time.Period; +import java.time.ZoneOffset; -import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc.createRounding; import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc.processDatetime; import static org.hamcrest.Matchers.containsString; @@ -27,73 +27,73 @@ public class DateTruncRoundingTests extends ESTestCase { public void testCreateRoundingDuration() { Rounding.Prepared rounding; - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createRounding(Duration.ofHours(0))); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Duration.ofHours(0))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> createRounding(Duration.ofHours(-10))); + e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Duration.ofHours(-10))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - rounding = createRounding(Duration.ofHours(1)); + rounding = createUtcRounding(Duration.ofHours(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.HOUR_OF_DAY), 0d); - rounding = createRounding(Duration.ofHours(10)); + rounding = createUtcRounding(Duration.ofHours(10)); assertEquals(10, rounding.roundingSize(Rounding.DateTimeUnit.HOUR_OF_DAY), 0d); - rounding = createRounding(Duration.ofMinutes(1)); + rounding = createUtcRounding(Duration.ofMinutes(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d); - rounding = createRounding(Duration.ofMinutes(100)); + rounding = createUtcRounding(Duration.ofMinutes(100)); assertEquals(100, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d); - rounding = createRounding(Duration.ofSeconds(1)); + rounding = createUtcRounding(Duration.ofSeconds(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), 0d); - rounding = createRounding(Duration.ofSeconds(120)); + rounding = createUtcRounding(Duration.ofSeconds(120)); assertEquals(120, rounding.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), 0d); - rounding = createRounding(Duration.ofSeconds(60).plusMinutes(5).plusHours(1)); + rounding = createUtcRounding(Duration.ofSeconds(60).plusMinutes(5).plusHours(1)); assertEquals(1 + 5 + 60, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d); } public void testCreateRoundingPeriod() { Rounding.Prepared rounding; - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createRounding(Period.ofMonths(0))); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Period.ofMonths(0))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> createRounding(Period.ofYears(-10))); + e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Period.ofYears(-10))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> createRounding(Period.of(0, 1, 1))); + e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Period.of(0, 1, 1))); assertThat(e.getMessage(), containsString("Time interval with multiple periods is not supported")); - rounding = createRounding(Period.ofDays(1)); + rounding = createUtcRounding(Period.ofDays(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.DAY_OF_MONTH), 0d); - rounding = createRounding(Period.ofDays(4)); + rounding = createUtcRounding(Period.ofDays(4)); assertEquals(4, rounding.roundingSize(Rounding.DateTimeUnit.DAY_OF_MONTH), 0d); - rounding = createRounding(Period.ofDays(7)); + rounding = createUtcRounding(Period.ofDays(7)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR), 0d); - rounding = createRounding(Period.ofMonths(1)); + rounding = createUtcRounding(Period.ofMonths(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MONTH_OF_YEAR), 0d); - rounding = createRounding(Period.ofMonths(3)); + rounding = createUtcRounding(Period.ofMonths(3)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.QUARTER_OF_YEAR), 0d); - rounding = createRounding(Period.ofMonths(5)); + rounding = createUtcRounding(Period.ofMonths(5)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MONTH_OF_YEAR), 0d); - rounding = createRounding(Period.ofYears(1)); + rounding = createUtcRounding(Period.ofYears(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.YEAR_OF_CENTURY), 0d); - rounding = createRounding(Period.ofYears(3)); + rounding = createUtcRounding(Period.ofYears(3)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.YEAR_OF_CENTURY), 0d); } public void testCreateRoundingNullInterval() { - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createRounding(null)); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding((Period) null)); assertThat(e.getMessage(), containsString("Time interval is not supported")); } @@ -102,11 +102,11 @@ public void testDateTruncFunction() { IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> processDatetime(ts, createRounding(Period.ofDays(-1))) + () -> processDatetime(ts, createUtcRounding(Period.ofDays(-1))) ); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> processDatetime(ts, createRounding(Duration.ofHours(-1)))); + e = expectThrows(IllegalArgumentException.class, () -> processDatetime(ts, createUtcRounding(Duration.ofHours(-1)))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); } @@ -114,4 +114,12 @@ private static long toMillis(String timestamp) { return Instant.parse(timestamp).toEpochMilli(); } + private static Rounding.Prepared createUtcRounding(Duration duration) { + return DateTrunc.createRounding(duration, ZoneOffset.UTC, null, null); + } + + private static Rounding.Prepared createUtcRounding(Period period) { + return DateTrunc.createRounding(period, ZoneOffset.UTC, null, null); + } + } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncSerializationTests.java index 3d1616ce29adf..c21af12f79e8f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncSerializationTests.java @@ -19,7 +19,7 @@ protected DateTrunc createTestInstance() { Source source = randomSource(); Expression interval = randomChild(); Expression field = randomChild(); - return new DateTrunc(source, interval, field); + return new DateTrunc(source, interval, field, configuration()); } @Override @@ -32,6 +32,6 @@ protected DateTrunc mutateInstance(DateTrunc instance) throws IOException { } else { field = randomValueOtherThan(field, AbstractExpressionSerializationTests::randomChild); } - return new DateTrunc(source, interval, field); + return new DateTrunc(source, interval, field, configuration()); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 76adc7e654941..2d8f41dfc83ee 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -14,8 +14,9 @@ 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.AbstractScalarFunctionTestCase; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; +import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; +import org.elasticsearch.xpack.esql.session.Configuration; import org.hamcrest.Matchers; import java.time.Duration; @@ -30,7 +31,7 @@ /** * Parameterized testing for {@link DateTrunc}. See also {@link DateTruncRoundingTests} for non-parametrized tests. */ -public class DateTruncTests extends AbstractScalarFunctionTestCase { +public class DateTruncTests extends AbstractConfigurationFunctionTestCase { public DateTruncTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); @@ -159,7 +160,7 @@ private static long toNanos(String timestamp) { } @Override - protected Expression build(Source source, List args) { - return new DateTrunc(source, args.get(0), args.get(1)); + protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { + return new DateTrunc(source, args.get(0), args.get(1), configuration); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java index df7e7189dce0d..47e6b87d245a0 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizerTests.java @@ -66,6 +66,7 @@ import org.elasticsearch.xpack.esql.plan.logical.local.EsqlProject; import org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation; import org.elasticsearch.xpack.esql.rule.RuleExecutor; +import org.elasticsearch.xpack.esql.session.Configuration; import org.elasticsearch.xpack.esql.stats.SearchStats; import org.hamcrest.Matchers; import org.junit.BeforeClass; @@ -1092,11 +1093,15 @@ protected LogicalPlan plan(String query) { return plan(query, analyzer); } - protected LogicalPlan localPlan(LogicalPlan plan, SearchStats searchStats) { - var localContext = new LocalLogicalOptimizerContext(EsqlTestUtils.TEST_CFG, FoldContext.small(), searchStats); + protected LogicalPlan localPlan(LogicalPlan plan, Configuration configuration, SearchStats searchStats) { + var localContext = new LocalLogicalOptimizerContext(configuration, FoldContext.small(), searchStats); return new LocalLogicalPlanOptimizer(localContext).localOptimize(plan); } + protected LogicalPlan localPlan(LogicalPlan plan, SearchStats searchStats) { + return localPlan(plan, EsqlTestUtils.TEST_CFG, searchStats); + } + private LogicalPlan localPlan(String query) { return localPlan(plan(query), TEST_SEARCH_STATS); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNullTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNullTests.java index e45782dcf80ee..1e2ca0ed289b0 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNullTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNullTests.java @@ -68,8 +68,8 @@ import java.util.List; import static org.elasticsearch.xpack.esql.EsqlTestUtils.L; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.esql.EsqlTestUtils.as; -import static org.elasticsearch.xpack.esql.EsqlTestUtils.configuration; import static org.elasticsearch.xpack.esql.EsqlTestUtils.getFieldAttribute; import static org.elasticsearch.xpack.esql.EsqlTestUtils.greaterThanOf; import static org.elasticsearch.xpack.esql.EsqlTestUtils.unboundLogicalOptimizerContext; @@ -98,7 +98,7 @@ public void testBasicNullFolding() { assertNullLiteral(foldNull(new Pow(EMPTY, Literal.NULL, Literal.NULL))); assertNullLiteral(foldNull(new DateFormat(EMPTY, Literal.NULL, Literal.NULL, null))); assertNullLiteral(foldNull(new DateParse(EMPTY, Literal.NULL, Literal.NULL, NULL))); - assertNullLiteral(foldNull(new DateTrunc(EMPTY, Literal.NULL, Literal.NULL))); + assertNullLiteral(foldNull(new DateTrunc(EMPTY, Literal.NULL, Literal.NULL, null))); assertNullLiteral(foldNull(new Substring(EMPTY, Literal.NULL, Literal.NULL, Literal.NULL))); } @@ -153,7 +153,7 @@ public void testGenericNullableExpression() { // regex assertNullLiteral(foldNull(new RLike(EMPTY, NULL, new RLikePattern("123")))); // date functions - assertNullLiteral(foldNull(new DateExtract(EMPTY, NULL, NULL, configuration("")))); + assertNullLiteral(foldNull(new DateExtract(EMPTY, NULL, NULL, TEST_CFG))); // math functions assertNullLiteral(foldNull(new Cos(EMPTY, NULL))); // string functions @@ -265,7 +265,7 @@ public void testNullFoldableDoesNotApplyToIsNullAndNotNull() { } public void testNullBucketGetsFolded() { - assertEquals(NULL, foldNull(new Bucket(EMPTY, NULL, NULL, NULL, NULL))); + assertEquals(NULL, foldNull(new Bucket(EMPTY, NULL, NULL, NULL, NULL, TEST_CFG))); } public void testNullCategorizeGroupingNotFolded() { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundToTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundToTests.java index aef6ba3c2abab..eae62ceea55cc 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundToTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundToTests.java @@ -24,14 +24,17 @@ import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.esql.plan.logical.Project; import org.elasticsearch.xpack.esql.plan.logical.TopN; +import org.elasticsearch.xpack.esql.session.Configuration; import org.elasticsearch.xpack.esql.stats.SearchStats; import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.esql.EsqlTestUtils.as; import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME; +import static org.hamcrest.CoreMatchers.equalTo; //@TestLogging(value = "org.elasticsearch.xpack.esql:TRACE", reason = "debug") public class ReplaceDateTruncBucketWithRoundToTests extends LocalLogicalPlanOptimizerTests { @@ -90,7 +93,8 @@ public void testSubstituteDateTruncInEvalWithRoundTo() { {} | limit 5 """, predicateString); - LogicalPlan localPlan = localPlan(query, searchStats); + Configuration configuration = TEST_CFG; + LogicalPlan localPlan = localPlan(plan(query), configuration, searchStats); Project project = as(localPlan, Project.class); TopN topN = as(project.child(), TopN.class); Eval eval = as(topN.child(), Eval.class); @@ -98,7 +102,7 @@ public void testSubstituteDateTruncInEvalWithRoundTo() { assertEquals(1, fields.size()); Alias a = fields.get(0); assertEquals("x", a.name()); - verifySubstitution(a, roundToPointsSize); + verifySubstitution(a, roundToPointsSize, configuration); LogicalPlan subPlan = predicateString.isEmpty() ? eval : eval.child(); EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } @@ -113,7 +117,8 @@ public void testSubstituteDateTruncInAggWithRoundTo() { {} | stats count(*) by x = date_trunc(1 day, hire_date) """, predicateString); - LogicalPlan localPlan = localPlan(query, searchStats); + Configuration configuration = TEST_CFG; + LogicalPlan localPlan = localPlan(plan(query), configuration, searchStats); Limit limit = as(localPlan, Limit.class); Aggregate aggregate = as(limit.child(), Aggregate.class); Eval eval = as(aggregate.child(), Eval.class); @@ -121,7 +126,7 @@ public void testSubstituteDateTruncInAggWithRoundTo() { assertEquals(1, fields.size()); Alias a = fields.get(0); assertEquals("x", a.name()); - verifySubstitution(a, roundToPointsSize); + verifySubstitution(a, roundToPointsSize, configuration); LogicalPlan subPlan = predicateString.isEmpty() ? eval : eval.child(); EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } @@ -136,7 +141,8 @@ public void testSubstituteBucketInAggWithRoundTo() { {} | stats count(*) by x = bucket(hire_date, 1 day) """, predicateString); - LogicalPlan localPlan = localPlan(query, searchStats); + Configuration configuration = TEST_CFG; + LogicalPlan localPlan = localPlan(plan(query), configuration, searchStats); Limit limit = as(localPlan, Limit.class); Aggregate aggregate = as(limit.child(), Aggregate.class); Eval eval = as(aggregate.child(), Eval.class); @@ -144,7 +150,7 @@ public void testSubstituteBucketInAggWithRoundTo() { assertEquals(1, fields.size()); Alias a = fields.get(0); assertEquals("x", a.name()); - verifySubstitution(a, roundToPointsSize); + verifySubstitution(a, roundToPointsSize, configuration); LogicalPlan subPlan = predicateString.isEmpty() ? eval : eval.child(); EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } @@ -166,7 +172,8 @@ public void testSubstituteDateTruncInEvalWithRoundToWithEvalRename() { | keep emp_no, {}, y | limit 5 """, predicateString, fieldName, fieldName); - LogicalPlan localPlan = localPlan(query, searchStats); + Configuration configuration = TEST_CFG; + LogicalPlan localPlan = localPlan(plan(query), configuration, searchStats); Project project = as(localPlan, Project.class); TopN topN = as(project.child(), TopN.class); Eval eval = as(topN.child(), Eval.class); @@ -174,7 +181,7 @@ public void testSubstituteDateTruncInEvalWithRoundToWithEvalRename() { assertEquals(dateTruncOnExpression ? 2 : 1, fields.size()); Alias a = fields.get(dateTruncOnExpression ? 1 : 0); assertEquals("y", a.name()); - verifySubstitution(a, roundToPointsSize); + verifySubstitution(a, roundToPointsSize, configuration); LogicalPlan subPlan = hasWhere ? eval.child() : eval; EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } @@ -193,7 +200,8 @@ public void testSubstituteBucketInAggWithRoundToWithEvalRename() { {} | stats count(*) by y = bucket({}, 1 day) """, predicateString, fieldName); - LogicalPlan localPlan = localPlan(query, searchStats); + Configuration configuration = TEST_CFG; + LogicalPlan localPlan = localPlan(plan(query), configuration, searchStats); Limit limit = as(localPlan, Limit.class); Aggregate aggregate = as(limit.child(), Aggregate.class); Eval eval = as(aggregate.child(), Eval.class); @@ -201,7 +209,7 @@ public void testSubstituteBucketInAggWithRoundToWithEvalRename() { assertEquals(dateTruncOnExpression ? 2 : 1, fields.size()); Alias a = fields.get(dateTruncOnExpression ? 1 : 0); assertEquals("y", a.name()); - verifySubstitution(a, roundToPointsSize); + verifySubstitution(a, roundToPointsSize, configuration); LogicalPlan subPlan = hasWhere ? eval.child() : eval; EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } @@ -220,7 +228,8 @@ public void testSubstituteDateTruncInAggWithRoundToWithEvalRename() { {} | stats count(*) by y = date_trunc(1 day, {}) """, predicateString, fieldName); - LogicalPlan localPlan = localPlan(query, searchStats); + Configuration configuration = TEST_CFG; + LogicalPlan localPlan = localPlan(plan(query), configuration, searchStats); Limit limit = as(localPlan, Limit.class); Aggregate aggregate = as(limit.child(), Aggregate.class); Eval eval = as(aggregate.child(), Eval.class); @@ -228,13 +237,13 @@ public void testSubstituteDateTruncInAggWithRoundToWithEvalRename() { assertEquals(dateTruncOnExpression ? 2 : 1, fields.size()); Alias a = fields.get(dateTruncOnExpression ? 1 : 0); assertEquals("y", a.name()); - verifySubstitution(a, roundToPointsSize); + verifySubstitution(a, roundToPointsSize, configuration); LogicalPlan subPlan = hasWhere ? eval.child() : eval; EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } } - private void verifySubstitution(Alias a, int roundToPointsSize) { + private void verifySubstitution(Alias a, int roundToPointsSize, Configuration configuration) { FieldAttribute fa = null; Expression e = a.child(); if (roundToPointsSize > 0) { @@ -243,16 +252,20 @@ private void verifySubstitution(Alias a, int roundToPointsSize) { assertEquals(roundToPointsSize, roundTo.points().size()); } else if (roundToPointsSize == 0) { if (e instanceof DateTrunc dateTrunc) { + assertThat(dateTrunc.configuration(), equalTo(configuration)); fa = as(dateTrunc.field(), FieldAttribute.class); } else if (e instanceof Bucket bucket) { + assertThat(bucket.configuration(), equalTo(configuration)); fa = as(bucket.field(), FieldAttribute.class); } else { fail(e.getClass() + " is not supported"); } } else { if (e instanceof DateTrunc dateTrunc) { + assertThat(dateTrunc.configuration(), equalTo(configuration)); assertTrue(dateTrunc.field() instanceof ReferenceAttribute); } else if (e instanceof Bucket bucket) { + assertThat(bucket.configuration(), equalTo(configuration)); assertTrue(bucket.field() instanceof ReferenceAttribute); } else { fail(e.getClass() + " is not supported"); @@ -264,11 +277,6 @@ private void verifySubstitution(Alias a, int roundToPointsSize) { } } - private LogicalPlan localPlan(String query, SearchStats searchStats) { - var plan = plan(query); - return localPlan(plan, searchStats); - } - private static SearchStats searchStats() { // create a SearchStats with min and max millis Map minValue = Map.of("hire_date", 1697804103360L); // 2023-10-20T12:15:03.360Z diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/TimeSeriesAggregateSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/TimeSeriesAggregateSerializationTests.java index 4d04bc557afba..c1de30ac71234 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/TimeSeriesAggregateSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/TimeSeriesAggregateSerializationTests.java @@ -23,7 +23,7 @@ protected TimeSeriesAggregate createTestInstance() { LogicalPlan child = randomChild(0); List groupings = randomFieldAttributes(0, 5, false).stream().map(a -> (Expression) a).toList(); List aggregates = AggregateSerializationTests.randomAggregates(); - Bucket timeBucket = BucketSerializationTests.createRandomBucket(); + Bucket timeBucket = BucketSerializationTests.createRandomBucket(configuration()); return new TimeSeriesAggregate(source, child, groupings, aggregates, timeBucket); } @@ -40,7 +40,7 @@ protected TimeSeriesAggregate mutateInstance(TimeSeriesAggregate instance) throw () -> randomFieldAttributes(0, 5, false).stream().map(a -> (Expression) a).toList() ); case 2 -> aggregates = randomValueOtherThan(aggregates, AggregateSerializationTests::randomAggregates); - case 3 -> timeBucket = randomValueOtherThan(timeBucket, BucketSerializationTests::createRandomBucket); + case 3 -> timeBucket = randomValueOtherThan(timeBucket, () -> BucketSerializationTests.createRandomBucket(configuration())); } return new TimeSeriesAggregate(instance.source(), child, groupings, aggregates, timeBucket); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/EvalMapperTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/EvalMapperTests.java index f3f14f2d2822d..92622106d7c3c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/EvalMapperTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/planner/EvalMapperTests.java @@ -127,7 +127,7 @@ public static List params() { new DateFormat(Source.EMPTY, datePattern, literal, TEST_CONFIG), new StartsWith(Source.EMPTY, literal, literal), new Substring(Source.EMPTY, literal, LONG, LONG), - new DateTrunc(Source.EMPTY, dateInterval, DATE) }) { + new DateTrunc(Source.EMPTY, dateInterval, DATE, TEST_CONFIG) }) { params.add(new Object[] { e.nodeString(), e }); } From ebd00dd4834c97ba0b5f5f1f26abeff34c699aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 29 Oct 2025 16:17:35 +0100 Subject: [PATCH 16/54] Added a ConfigurationFUnction interface and fixed tests with static configurations --- .../function/ConfigurationFunction.java | 28 +++++++++++++++++++ .../function/EsqlFunctionRegistry.java | 3 +- .../expression/function/grouping/Bucket.java | 10 ++----- .../expression/function/grouping/TBucket.java | 13 ++++----- .../scalar/EsqlConfigurationFunction.java | 9 +++++- .../function/grouping/BucketTests.java | 12 ++++---- .../function/grouping/TBucketTests.java | 4 +-- .../function/scalar/date/DateTruncTests.java | 8 +++--- 8 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ConfigurationFunction.java diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ConfigurationFunction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ConfigurationFunction.java new file mode 100644 index 0000000000000..b1aaffa9ac67b --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ConfigurationFunction.java @@ -0,0 +1,28 @@ +/* + * 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; + +/** + * Marker interface indicating that a function needs a Configuration object as its last parameter. + *

+ * Extend {@link org.elasticsearch.xpack.esql.expression.function.scalar.EsqlConfigurationFunction} instead if possible. + * It automatically takes care of: + *

+ *
    + *
  • The Configuration field
  • + *
  • HashCode
  • + *
  • Equals
  • + *
+ *

+ * If overridden directly, take a look at {@link org.elasticsearch.xpack.esql.expression.function.scalar.EsqlConfigurationFunction} + * and add or update the required methods. + *

+ */ +public interface ConfigurationFunction { + +} 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 7f29bb652e8b2..9a66c0ea16670 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 @@ -70,7 +70,6 @@ import org.elasticsearch.xpack.esql.expression.function.grouping.TBucket; import org.elasticsearch.xpack.esql.expression.function.inference.TextEmbedding; import org.elasticsearch.xpack.esql.expression.function.scalar.Clamp; -import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlConfigurationFunction; import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.Case; import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.ClampMax; import org.elasticsearch.xpack.esql.expression.function.scalar.conditional.ClampMin; @@ -773,7 +772,7 @@ public static FunctionDescription description(FunctionDefinition def) { if (TimestampAware.class.isAssignableFrom(def.clazz())) { countOfParamsToDescribe--; // skip the implicit @timestamp parameter (last or last before Configuration) } - if (EsqlConfigurationFunction.class.isAssignableFrom(def.clazz())) { + if (ConfigurationFunction.class.isAssignableFrom(def.clazz())) { // this isn't enforced by the contract, but the convention is: func(..., Expression timestamp, Configuration config) assert Configuration.class.isAssignableFrom(params[params.length - 1].getType()) : "The configuration parameter must be the last argument of an EsqlConfigurationFunction definition"; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java index 11b6bdcfeeee0..9d66392a08f43 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java @@ -25,6 +25,7 @@ import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.Foldables; +import org.elasticsearch.xpack.esql.expression.function.ConfigurationFunction; import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.FunctionType; @@ -62,7 +63,8 @@ public class Bucket extends GroupingFunction.EvaluatableGroupingFunction implements PostOptimizationVerificationAware, - TwoOptionalArguments { + TwoOptionalArguments, + ConfigurationFunction { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Bucket", Bucket::new); // TODO maybe we should just cover the whole of representable dates here - like ten years, 100 years, 1000 years, all the way up. @@ -88,12 +90,6 @@ public class Bucket extends GroupingFunction.EvaluatableGroupingFunction Rounding.builder(TimeValue.timeValueMillis(10)), Rounding.builder(TimeValue.timeValueMillis(1)), }; - /* - * As Bucket already extends GroupingFunction, it can't extend EsqlConfigurationFunction, so we had to replicate here: - * - The Configuration field - * - HashCode - * - Equals - */ private final Configuration configuration; private final Expression field; private final Expression buckets; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucket.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucket.java index 2377f89048c09..1f1af8137b251 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucket.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucket.java @@ -14,6 +14,7 @@ import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.SurrogateExpression; +import org.elasticsearch.xpack.esql.expression.function.ConfigurationFunction; import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.FunctionType; @@ -32,15 +33,13 @@ /** * Splits dates into a given number of buckets. The span is derived from a time range provided. */ -public class TBucket extends GroupingFunction.EvaluatableGroupingFunction implements SurrogateExpression, TimestampAware { +public class TBucket extends GroupingFunction.EvaluatableGroupingFunction + implements + SurrogateExpression, + TimestampAware, + ConfigurationFunction { public static final String NAME = "TBucket"; - /* - * As Bucket already extends GroupingFunction, it can't extend EsqlConfigurationFunction, so we had to replicate here: - * - The Configuration field - * - HashCode - * - Equals - */ private final Configuration configuration; private final Expression buckets; private final Expression timestamp; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/EsqlConfigurationFunction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/EsqlConfigurationFunction.java index 4b5fcddcf0dfa..14636fc35b679 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/EsqlConfigurationFunction.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/EsqlConfigurationFunction.java @@ -9,12 +9,19 @@ import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.expression.function.ConfigurationFunction; import org.elasticsearch.xpack.esql.session.Configuration; import java.util.List; import java.util.Objects; -public abstract class EsqlConfigurationFunction extends EsqlScalarFunction { +/** + * Implementation of {@link ConfigurationFunction}. + *

+ * Extend this class if possible instead of the base interface. + *

+ */ +public abstract class EsqlConfigurationFunction extends EsqlScalarFunction implements ConfigurationFunction { private final Configuration configuration; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java index 286759a5ed861..2f27b41faddba 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java @@ -114,7 +114,7 @@ private static void dateCases(List suppliers, String name, Lon + "rounding=Rounding[DAY_OF_MONTH in Z][fixed to midnight]]", DataType.DATETIME, resultsMatcher(args) - ); + ).withStaticConfiguration(); })); // same as above, but a low bucket count and datetime bounds that match it (at hour span) suppliers.add(new TestCaseSupplier(name, List.of(DataType.DATETIME, DataType.INTEGER, fromType, toType), () -> { @@ -128,7 +128,7 @@ private static void dateCases(List suppliers, String name, Lon "DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding[3600000 in Z][fixed]]", DataType.DATETIME, equalTo(Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY).build().prepareForUnknown().round(date.getAsLong())) - ); + ).withStaticConfiguration(); })); } } @@ -161,7 +161,7 @@ private static void dateCasesWithSpan( "DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding" + spanStr + "]", DataType.DATETIME, resultsMatcher(args) - ); + ).withStaticConfiguration(); })); } @@ -182,7 +182,7 @@ private static void dateNanosCasesWithSpan( Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, resultsMatcher(args) - ); + ).withStaticConfiguration(); })); } @@ -201,7 +201,7 @@ private static void dateNanosCases(List suppliers, String name Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, resultsMatcher(args) - ); + ).withStaticConfiguration(); })); // same as above, but a low bucket count and datetime bounds that match it (at hour span) suppliers.add(new TestCaseSupplier(name, List.of(DataType.DATE_NANOS, DataType.INTEGER, fromType, toType), () -> { @@ -215,7 +215,7 @@ private static void dateNanosCases(List suppliers, String name Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, equalTo(Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY).build().prepareForUnknown().round(date.getAsLong())) - ); + ).withStaticConfiguration(); })); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java index 648f8f214ef39..0cb2457dadd50 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java @@ -95,7 +95,7 @@ private static void dateCasesWithSpan( "DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding" + spanStr + "]", DataType.DATETIME, resultsMatcher(args) - ); + ).withStaticConfiguration(); })); } @@ -115,7 +115,7 @@ private static void dateNanosCasesWithSpan( Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, resultsMatcher(args) - ); + ).withStaticConfiguration(); })); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 2d8f41dfc83ee..375562ae534a5 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -76,7 +76,7 @@ private static List ofDatePeriod(Period period, long value, St Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, equalTo(toMillis(expectedDate)) - ) + ).withStaticConfiguration() ), new TestCaseSupplier( List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), @@ -88,7 +88,7 @@ private static List ofDatePeriod(Period period, long value, St Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, equalTo(toNanos(expectedDate)) - ) + ).withStaticConfiguration() ) ); } @@ -105,7 +105,7 @@ private static List ofDuration(Duration duration, long value, Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, equalTo(toMillis(expectedDate)) - ) + ).withStaticConfiguration() ), new TestCaseSupplier( List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), @@ -117,7 +117,7 @@ private static List ofDuration(Duration duration, long value, Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, equalTo(toNanos(expectedDate)) - ) + ).withStaticConfiguration() ) ); } From de30da75af15ef640bd33df19061b2430128af3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 29 Oct 2025 19:08:41 +0100 Subject: [PATCH 17/54] WIP: Initial DateTrunc unit tests --- .../xpack/esql/ConfigurationBuilder.java | 151 ++++++++++++++++++ .../xpack/esql/ConfigurationTestUtils.java | 1 + .../function/scalar/date/DateTruncTests.java | 72 ++++++--- 3 files changed, 199 insertions(+), 25 deletions(-) create mode 100644 x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationBuilder.java diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationBuilder.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationBuilder.java new file mode 100644 index 0000000000000..611b16ad52bea --- /dev/null +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationBuilder.java @@ -0,0 +1,151 @@ +/* + * 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; + +import org.elasticsearch.xpack.esql.plugin.QueryPragmas; +import org.elasticsearch.xpack.esql.session.Configuration; + +import java.time.ZoneId; +import java.util.Locale; +import java.util.Map; + +/** + * Builder to modify configurations on tests. + *

+ * The {@link Configuration#now()} field is actually modified, so built configurations will also differ there. + *

+ */ +public class ConfigurationBuilder { + + private String clusterName; + private String username; + private ZoneId zoneId; + + private QueryPragmas pragmas; + + private int resultTruncationMaxSizeRegular; + private int resultTruncationDefaultSizeRegular; + private int resultTruncationMaxSizeTimeseries; + private int resultTruncationDefaultSizeTimeseries; + + private Locale locale; + + private String query; + + private boolean profile; + private boolean allowPartialResults; + + private Map> tables; + private long queryStartTimeNanos; + + public ConfigurationBuilder(Configuration configuration) { + clusterName = configuration.clusterName(); + username = configuration.username(); + zoneId = configuration.zoneId(); + pragmas = configuration.pragmas(); + resultTruncationMaxSizeRegular = configuration.resultTruncationMaxSize(false); + resultTruncationDefaultSizeRegular = configuration.resultTruncationDefaultSize(false); + resultTruncationMaxSizeTimeseries = configuration.resultTruncationMaxSize(true); + resultTruncationDefaultSizeTimeseries = configuration.resultTruncationDefaultSize(true); + locale = configuration.locale(); + query = configuration.query(); + profile = configuration.profile(); + allowPartialResults = configuration.allowPartialResults(); + tables = configuration.tables(); + queryStartTimeNanos = configuration.queryStartTimeNanos(); + } + + public ConfigurationBuilder clusterName(String clusterName) { + this.clusterName = clusterName; + return this; + } + + public ConfigurationBuilder username(String username) { + this.username = username; + return this; + } + + public ConfigurationBuilder zoneId(ZoneId zoneId) { + this.zoneId = zoneId; + return this; + } + + public ConfigurationBuilder pragmas(QueryPragmas pragmas) { + this.pragmas = pragmas; + return this; + } + + public ConfigurationBuilder resultTruncationMaxSizeRegular(int resultTruncationMaxSizeRegular) { + this.resultTruncationMaxSizeRegular = resultTruncationMaxSizeRegular; + return this; + } + + public ConfigurationBuilder resultTruncationDefaultSizeRegular(int resultTruncationDefaultSizeRegular) { + this.resultTruncationDefaultSizeRegular = resultTruncationDefaultSizeRegular; + return this; + } + + public ConfigurationBuilder resultTruncationMaxSizeTimeseries(int resultTruncationMaxSizeTimeseries) { + this.resultTruncationMaxSizeTimeseries = resultTruncationMaxSizeTimeseries; + return this; + } + + public ConfigurationBuilder resultTruncationDefaultSizeTimeseries(int resultTruncationDefaultSizeTimeseries) { + this.resultTruncationDefaultSizeTimeseries = resultTruncationDefaultSizeTimeseries; + return this; + } + + public ConfigurationBuilder locale(Locale locale) { + this.locale = locale; + return this; + } + + public ConfigurationBuilder query(String query) { + this.query = query; + return this; + } + + public ConfigurationBuilder profile(boolean profile) { + this.profile = profile; + return this; + } + + public ConfigurationBuilder allowPartialResults(boolean allowPartialResults) { + this.allowPartialResults = allowPartialResults; + return this; + } + + public ConfigurationBuilder tables(Map> tables) { + this.tables = tables; + return this; + } + + public ConfigurationBuilder queryStartTimeNanos(long queryStartTimeNanos) { + this.queryStartTimeNanos = queryStartTimeNanos; + return this; + } + + public Configuration build() { + return new Configuration( + zoneId, + locale, + username, + clusterName, + pragmas, + resultTruncationMaxSizeRegular, + resultTruncationDefaultSizeRegular, + query, + profile, + tables, + queryStartTimeNanos, + allowPartialResults, + resultTruncationMaxSizeTimeseries, + resultTruncationDefaultSizeTimeseries + ); + } +} diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java index ce59002fda037..36445291eeee9 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java @@ -40,6 +40,7 @@ import static org.elasticsearch.test.ESTestCase.randomRealisticUnicodeOfLength; import static org.elasticsearch.test.ESTestCase.randomZone; import static org.elasticsearch.xpack.esql.EsqlTestUtils.randomLiteral; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.tables; import static org.elasticsearch.xpack.esql.session.Configuration.QUERY_COMPRESS_THRESHOLD_CHARS; /** diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 375562ae534a5..d1195b29bc338 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -11,6 +11,7 @@ import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.elasticsearch.common.time.DateUtils; +import org.elasticsearch.xpack.esql.ConfigurationBuilder; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -22,10 +23,13 @@ import java.time.Duration; import java.time.Instant; import java.time.Period; +import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; +import static org.elasticsearch.xpack.esql.ConfigurationTestUtils.randomConfiguration; +import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; import static org.hamcrest.Matchers.equalTo; /** @@ -39,85 +43,99 @@ public DateTruncTests(@Name("TestCase") Supplier test @ParametersFactory public static Iterable parameters() { - long ts = toMillis("2023-02-17T10:25:33.38Z"); List suppliers = new ArrayList<>(); - suppliers.addAll(ofDatePeriod(Period.ofDays(1), ts, "2023-02-17T00:00:00.00Z")); - suppliers.addAll(ofDatePeriod(Period.ofMonths(1), ts, "2023-02-01T00:00:00.00Z")); - suppliers.addAll(ofDatePeriod(Period.ofYears(1), ts, "2023-01-01T00:00:00.00Z")); - suppliers.addAll(ofDatePeriod(Period.ofDays(10), ts, "2023-02-12T00:00:00.00Z")); + + /// + /// UTC checks + /// + String ts = "2023-02-17T10:25:33.38Z"; + suppliers.addAll(ofDatePeriod(Period.ofDays(1), ts, "UTC", "2023-02-17T00:00:00.00Z")); + suppliers.addAll(ofDatePeriod(Period.ofMonths(1), ts, "UTC", "2023-02-01T00:00:00.00Z")); + suppliers.addAll(ofDatePeriod(Period.ofYears(1), ts, "UTC", "2023-01-01T00:00:00.00Z")); + suppliers.addAll(ofDatePeriod(Period.ofDays(10), ts, "UTC", "2023-02-12T00:00:00.00Z")); // 7 days period should return weekly rounding - suppliers.addAll(ofDatePeriod(Period.ofDays(7), ts, "2023-02-13T00:00:00.00Z")); + suppliers.addAll(ofDatePeriod(Period.ofDays(7), ts, "UTC", "2023-02-13T00:00:00.00Z")); // 3 months period should return quarterly - suppliers.addAll(ofDatePeriod(Period.ofMonths(3), ts, "2023-01-01T00:00:00.00Z")); - suppliers.addAll(ofDuration(Duration.ofHours(1), ts, "2023-02-17T10:00:00.00Z")); - suppliers.addAll(ofDuration(Duration.ofMinutes(1), ts, "2023-02-17T10:25:00.00Z")); - suppliers.addAll(ofDuration(Duration.ofSeconds(1), ts, "2023-02-17T10:25:33.00Z")); - suppliers.addAll(ofDuration(Duration.ofHours(3), ts, "2023-02-17T09:00:00.00Z")); - suppliers.addAll(ofDuration(Duration.ofMinutes(15), ts, "2023-02-17T10:15:00.00Z")); - suppliers.addAll(ofDuration(Duration.ofSeconds(30), ts, "2023-02-17T10:25:30.00Z")); + suppliers.addAll(ofDatePeriod(Period.ofMonths(3), ts, "UTC", "2023-01-01T00:00:00.00Z")); + suppliers.addAll(ofDuration(Duration.ofHours(1), ts, "UTC", "2023-02-17T10:00:00.00Z")); + suppliers.addAll(ofDuration(Duration.ofMinutes(1), ts, "UTC", "2023-02-17T10:25:00.00Z")); + suppliers.addAll(ofDuration(Duration.ofSeconds(1), ts, "UTC", "2023-02-17T10:25:33.00Z")); + suppliers.addAll(ofDuration(Duration.ofHours(3), ts, "UTC", "2023-02-17T09:00:00.00Z")); + suppliers.addAll(ofDuration(Duration.ofMinutes(15), ts, "UTC", "2023-02-17T10:15:00.00Z")); + suppliers.addAll(ofDuration(Duration.ofSeconds(30), ts, "UTC", "2023-02-17T10:25:30.00Z")); suppliers.add(randomSecond()); // arbitrary period of months and years - suppliers.addAll(ofDatePeriod(Period.ofMonths(7), ts, "2022-11-01T00:00:00.00Z")); - suppliers.addAll(ofDatePeriod(Period.ofYears(5), ts, "2021-01-01T00:00:00.00Z")); + suppliers.addAll(ofDatePeriod(Period.ofMonths(7), ts, "UTC", "2022-11-01T00:00:00.00Z")); + suppliers.addAll(ofDatePeriod(Period.ofYears(5), ts, "UTC", "2021-01-01T00:00:00.00Z")); + + /// + /// Timezones + /// + // TODO: Add tests for every case. Order them correctly + suppliers.addAll(ofDatePeriod(Period.ofMonths(3), "2023-01-01T00:00:00.00Z", "-01:00", "2022-10-01T01:00:00.00Z")); return parameterSuppliersFromTypedDataWithDefaultChecks(true, suppliers); } - private static List ofDatePeriod(Period period, long value, String expectedDate) { + private static List ofDatePeriod(Period period, String inputDate, String zoneId, String expectedDate) { return List.of( new TestCaseSupplier( + "period, millis; " + zoneId, List.of(DataType.DATE_PERIOD, DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of( new TestCaseSupplier.TypedData(period, DataType.DATE_PERIOD, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(value, DataType.DATETIME, "date") + new TestCaseSupplier.TypedData(toMillis(inputDate), DataType.DATETIME, "date") ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, equalTo(toMillis(expectedDate)) - ).withStaticConfiguration() + ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) ), new TestCaseSupplier( + "period, nanos; " + zoneId, List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( List.of( new TestCaseSupplier.TypedData(period, DataType.DATE_PERIOD, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(value), DataType.DATE_NANOS, "date") + new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(toMillis(inputDate)), DataType.DATE_NANOS, "date") ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, equalTo(toNanos(expectedDate)) - ).withStaticConfiguration() + ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) ) ); } - private static List ofDuration(Duration duration, long value, String expectedDate) { + private static List ofDuration(Duration duration, String inputDate, String zoneId, String expectedDate) { return List.of( new TestCaseSupplier( + "period, millis; " + zoneId, List.of(DataType.TIME_DURATION, DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of( new TestCaseSupplier.TypedData(duration, DataType.TIME_DURATION, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(value, DataType.DATETIME, "date") + new TestCaseSupplier.TypedData(toMillis(inputDate), DataType.DATETIME, "date") ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, equalTo(toMillis(expectedDate)) - ).withStaticConfiguration() + ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) ), new TestCaseSupplier( + "period, nanos; " + zoneId, List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( List.of( new TestCaseSupplier.TypedData(duration, DataType.TIME_DURATION, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(value), DataType.DATE_NANOS, "date") + new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(toMillis(inputDate)), DataType.DATE_NANOS, "date") ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, equalTo(toNanos(expectedDate)) - ).withStaticConfiguration() + ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) ) ); } @@ -163,4 +181,8 @@ private static long toNanos(String timestamp) { protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { return new DateTrunc(source, args.get(0), args.get(1), configuration); } + + private static Configuration configurationForTimezone(String zoneId) { + return new ConfigurationBuilder(randomConfiguration()).query(TEST_SOURCE.text()).zoneId(ZoneId.of(zoneId)).build(); + } } From dcce704049df75c948182e1e9fcb2c0fec16dadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Thu, 30 Oct 2025 14:38:59 +0100 Subject: [PATCH 18/54] Reuse DateTrunc tests in Bucket and TBucket --- .../function/grouping/BucketTests.java | 80 +++++++++ .../function/grouping/TBucketTests.java | 80 +++++++++ ...AbstractConfigurationFunctionTestCase.java | 7 + .../function/scalar/date/DateTruncTests.java | 155 +++++++++++------- 4 files changed, 267 insertions(+), 55 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java index 2f27b41faddba..d6a5a4bcb3d71 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java @@ -33,6 +33,9 @@ import java.util.function.LongSupplier; import java.util.function.Supplier; +import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; +import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTruncTests.makeTruncDurationTestCases; +import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTruncTests.makeTruncPeriodTestCases; import static org.hamcrest.Matchers.equalTo; public class BucketTests extends AbstractConfigurationFunctionTestCase { @@ -77,6 +80,7 @@ public static Iterable parameters() { Duration.ofDays(1L), "[86400000 in Z][fixed]" ); + dateTruncCases(suppliers); numberCases(suppliers, "fixed long", DataType.LONG, () -> 100L); numberCasesWithSpan(suppliers, "fixed long with span", DataType.LONG, () -> 100L); numberCases(suppliers, "fixed int", DataType.INTEGER, () -> 100); @@ -134,6 +138,82 @@ private static void dateCases(List suppliers, String name, Lon } } + private static void dateTruncCases(List suppliers) { + makeTruncPeriodTestCases().stream() + .map( + data -> List.of( + new TestCaseSupplier( + "period, millis; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), + List.of(DataType.DATETIME, DataType.DATE_PERIOD), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "field"), + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral() + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + equalTo(data.expectedDateAsMillis()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ), + new TestCaseSupplier( + "period, nanos; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), + List.of(DataType.DATE_NANOS, DataType.DATE_PERIOD), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData( + DateUtils.toNanoSeconds(data.inputDateAsMillis()), + DataType.DATE_NANOS, + "field" + ), + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral() + ), + Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATE_NANOS, + equalTo(DateUtils.toLong(Instant.parse(data.expectedDate()))) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ) + ) + .forEach(suppliers::addAll); + + makeTruncDurationTestCases().stream() + .map( + data -> List.of( + new TestCaseSupplier( + "duration, millis; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), + List.of(DataType.DATETIME, DataType.TIME_DURATION), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "field"), + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral() + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + equalTo(data.expectedDateAsMillis()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ), + new TestCaseSupplier( + "duration, nanos; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), + List.of(DataType.DATE_NANOS, DataType.TIME_DURATION), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData( + DateUtils.toNanoSeconds(data.inputDateAsMillis()), + DataType.DATE_NANOS, + "field" + ), + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral() + ), + Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATE_NANOS, + equalTo(DateUtils.toLong(Instant.parse(data.expectedDate()))) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ) + ) + .forEach(suppliers::addAll); + } + private static TestCaseSupplier.TypedData dateBound(String name, DataType type, String date) { Object value; if (type == DataType.DATETIME) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java index 0cb2457dadd50..1f051b4a8afca 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java @@ -32,6 +32,9 @@ import java.util.function.LongSupplier; import java.util.function.Supplier; +import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; +import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTruncTests.makeTruncDurationTestCases; +import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTruncTests.makeTruncPeriodTestCases; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; @@ -74,6 +77,7 @@ public static Iterable parameters() { DataType.TIME_DURATION, Duration.ofDays(1L) ); + dateTruncCases(suppliers); return parameterSuppliersFromTypedData(suppliers); } @@ -119,6 +123,82 @@ private static void dateNanosCasesWithSpan( })); } + private static void dateTruncCases(List suppliers) { + makeTruncPeriodTestCases().stream() + .map( + data -> List.of( + new TestCaseSupplier( + "period, millis; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), + List.of(DataType.DATE_PERIOD, DataType.DATETIME), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "@timestamp") + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + equalTo(data.expectedDateAsMillis()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ), + new TestCaseSupplier( + "period, nanos; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), + List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), + new TestCaseSupplier.TypedData( + DateUtils.toNanoSeconds(data.inputDateAsMillis()), + DataType.DATE_NANOS, + "@timestamp" + ) + ), + Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATE_NANOS, + equalTo(DateUtils.toLong(Instant.parse(data.expectedDate()))) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ) + ) + .forEach(suppliers::addAll); + + makeTruncDurationTestCases().stream() + .map( + data -> List.of( + new TestCaseSupplier( + "duration, millis; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), + List.of(DataType.TIME_DURATION, DataType.DATETIME), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "@timestamp") + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + equalTo(data.expectedDateAsMillis()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ), + new TestCaseSupplier( + "duration, nanos; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), + List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), + new TestCaseSupplier.TypedData( + DateUtils.toNanoSeconds(data.inputDateAsMillis()), + DataType.DATE_NANOS, + "@timestamp" + ) + ), + Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATE_NANOS, + equalTo(DateUtils.toLong(Instant.parse(data.expectedDate()))) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ) + ) + .forEach(suppliers::addAll); + } + private static Matcher resultsMatcher(List typedData) { if (typedData.get(1).type() == DataType.DATE_NANOS) { long nanos = ((Number) typedData.get(1).data()).longValue(); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java index 116e8ee34e942..5a677c076f5dc 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java @@ -7,16 +7,19 @@ package org.elasticsearch.xpack.esql.expression.function.scalar; +import org.elasticsearch.xpack.esql.ConfigurationBuilder; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; import org.elasticsearch.xpack.esql.session.Configuration; +import java.time.ZoneId; import java.util.List; import static org.elasticsearch.xpack.esql.ConfigurationTestUtils.randomConfiguration; import static org.elasticsearch.xpack.esql.ConfigurationTestUtils.randomTables; import static org.elasticsearch.xpack.esql.SerializationTestUtils.assertSerialization; +import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; public abstract class AbstractConfigurationFunctionTestCase extends AbstractScalarFunctionTestCase { protected abstract Expression buildWithConfiguration(Source source, List args, Configuration configuration); @@ -43,4 +46,8 @@ public void testSerializationWithConfiguration() { Expression differentExpr = buildWithConfiguration(testCase.getSource(), testCase.getDataAsFields(), differentConfig); assertNotEquals(expr, differentExpr); } + + protected static Configuration configurationForTimezone(ZoneId zoneId) { + return new ConfigurationBuilder(randomConfiguration()).query(TEST_SOURCE.text()).zoneId(zoneId).build(); + } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index d1195b29bc338..9855bf810028b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -11,7 +11,7 @@ import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.elasticsearch.common.time.DateUtils; -import org.elasticsearch.xpack.esql.ConfigurationBuilder; +import org.elasticsearch.core.Nullable; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -28,7 +28,6 @@ import java.util.List; import java.util.function.Supplier; -import static org.elasticsearch.xpack.esql.ConfigurationTestUtils.randomConfiguration; import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; import static org.hamcrest.Matchers.equalTo; @@ -45,97 +44,147 @@ public DateTruncTests(@Name("TestCase") Supplier test public static Iterable parameters() { List suppliers = new ArrayList<>(); - /// - /// UTC checks - /// - String ts = "2023-02-17T10:25:33.38Z"; - suppliers.addAll(ofDatePeriod(Period.ofDays(1), ts, "UTC", "2023-02-17T00:00:00.00Z")); - suppliers.addAll(ofDatePeriod(Period.ofMonths(1), ts, "UTC", "2023-02-01T00:00:00.00Z")); - suppliers.addAll(ofDatePeriod(Period.ofYears(1), ts, "UTC", "2023-01-01T00:00:00.00Z")); - suppliers.addAll(ofDatePeriod(Period.ofDays(10), ts, "UTC", "2023-02-12T00:00:00.00Z")); - // 7 days period should return weekly rounding - suppliers.addAll(ofDatePeriod(Period.ofDays(7), ts, "UTC", "2023-02-13T00:00:00.00Z")); - // 3 months period should return quarterly - suppliers.addAll(ofDatePeriod(Period.ofMonths(3), ts, "UTC", "2023-01-01T00:00:00.00Z")); - suppliers.addAll(ofDuration(Duration.ofHours(1), ts, "UTC", "2023-02-17T10:00:00.00Z")); - suppliers.addAll(ofDuration(Duration.ofMinutes(1), ts, "UTC", "2023-02-17T10:25:00.00Z")); - suppliers.addAll(ofDuration(Duration.ofSeconds(1), ts, "UTC", "2023-02-17T10:25:33.00Z")); - suppliers.addAll(ofDuration(Duration.ofHours(3), ts, "UTC", "2023-02-17T09:00:00.00Z")); - suppliers.addAll(ofDuration(Duration.ofMinutes(15), ts, "UTC", "2023-02-17T10:15:00.00Z")); - suppliers.addAll(ofDuration(Duration.ofSeconds(30), ts, "UTC", "2023-02-17T10:25:30.00Z")); + makeTruncPeriodTestCases().stream().map(DateTruncTests::ofDatePeriod).forEach(suppliers::addAll); + makeTruncDurationTestCases().stream().map(DateTruncTests::ofDuration).forEach(suppliers::addAll); + suppliers.add(randomSecond()); - // arbitrary period of months and years - suppliers.addAll(ofDatePeriod(Period.ofMonths(7), ts, "UTC", "2022-11-01T00:00:00.00Z")); - suppliers.addAll(ofDatePeriod(Period.ofYears(5), ts, "UTC", "2021-01-01T00:00:00.00Z")); + return parameterSuppliersFromTypedDataWithDefaultChecks(true, suppliers); + } + + public record PeriodTestCaseData(Period period, String inputDate, @Nullable String zoneIdString, String expectedDate) { + public ZoneId zoneId() { + return zoneIdString == null ? randomZone() : ZoneId.of(zoneIdString); + } - /// - /// Timezones - /// - // TODO: Add tests for every case. Order them correctly - suppliers.addAll(ofDatePeriod(Period.ofMonths(3), "2023-01-01T00:00:00.00Z", "-01:00", "2022-10-01T01:00:00.00Z")); + public long inputDateAsMillis() { + return Instant.parse(inputDate).toEpochMilli(); + } - return parameterSuppliersFromTypedDataWithDefaultChecks(true, suppliers); + public long expectedDateAsMillis() { + return Instant.parse(expectedDate).toEpochMilli(); + } + } + + public record DurationTestCaseData(Duration duration, String inputDate, @Nullable String zoneIdString, String expectedDate) { + public ZoneId zoneId() { + return zoneIdString == null ? randomZone() : ZoneId.of(zoneIdString); + } + + public long inputDateAsMillis() { + return Instant.parse(inputDate).toEpochMilli(); + } + + public long expectedDateAsMillis() { + return Instant.parse(expectedDate).toEpochMilli(); + } + } + + public static List makeTruncDurationTestCases() { + String ts = "2023-02-17T10:25:33.38Z"; + return List.of( + /// + /// Timezone agnostic (<2h intervals) + /// + new DurationTestCaseData(Duration.ofMillis(100), ts, null, "2023-02-17T10:25:33.30Z"), + new DurationTestCaseData(Duration.ofSeconds(1), ts, null, "2023-02-17T10:25:33Z"), + new DurationTestCaseData(Duration.ofMinutes(1), ts, null, "2023-02-17T10:25:00Z"), + new DurationTestCaseData(Duration.ofHours(1), ts, null, "2023-02-17T10:00:00Z"), + new DurationTestCaseData(Duration.ofSeconds(30), ts, null, "2023-02-17T10:25:30Z"), + new DurationTestCaseData(Duration.ofMinutes(15), ts, null, "2023-02-17T10:15:00Z"), + + /// + /// Timezone dependent (>=2h intervals) + /// + new DurationTestCaseData(Duration.ofHours(3), ts, "UTC", "2023-02-17T09:00:00Z"), + new DurationTestCaseData(Duration.ofHours(3), ts, "-02:00", "2023-02-17T08:00:00Z"), + new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z") + ); } - private static List ofDatePeriod(Period period, String inputDate, String zoneId, String expectedDate) { + public static List makeTruncPeriodTestCases() { + String ts = "2023-02-17T10:25:33.38Z"; + return List.of( + /// + /// UTC + /// + new PeriodTestCaseData(Period.ofDays(1), ts, "UTC", "2023-02-17T00:00:00.00Z"), + new PeriodTestCaseData(Period.ofMonths(1), ts, "UTC", "2023-02-01T00:00:00.00Z"), + new PeriodTestCaseData(Period.ofYears(1), ts, "UTC", "2023-01-01T00:00:00.00Z"), + new PeriodTestCaseData(Period.ofDays(10), ts, "UTC", "2023-02-12T00:00:00.00Z"), + // 7 days period should return weekly rounding + new PeriodTestCaseData(Period.ofDays(7), ts, "UTC", "2023-02-13T00:00:00.00Z"), + // 3 months period should return quarterly + new PeriodTestCaseData(Period.ofMonths(3), ts, "UTC", "2023-01-01T00:00:00.00Z"), + // arbitrary period of months and years + new PeriodTestCaseData(Period.ofMonths(7), ts, "UTC", "2022-11-01T00:00:00.00Z"), + new PeriodTestCaseData(Period.ofYears(5), ts, "UTC", "2021-01-01T00:00:00.00Z"), + + /// + /// Timezones + /// + new PeriodTestCaseData(Period.ofMonths(3), "2023-01-01T00:00:00.00Z", "-01:00", "2022-10-01T01:00:00.00Z") + ); + } + + private static List ofDatePeriod(PeriodTestCaseData data) { return List.of( new TestCaseSupplier( - "period, millis; " + zoneId, + "period, millis; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), List.of(DataType.DATE_PERIOD, DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData(period, DataType.DATE_PERIOD, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(toMillis(inputDate), DataType.DATETIME, "date") + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "date") ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - equalTo(toMillis(expectedDate)) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) + equalTo(data.expectedDateAsMillis()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( - "period, nanos; " + zoneId, + "period, nanos; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData(period, DataType.DATE_PERIOD, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(toMillis(inputDate)), DataType.DATE_NANOS, "date") + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(data.inputDateAsMillis()), DataType.DATE_NANOS, "date") ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - equalTo(toNanos(expectedDate)) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) + equalTo(toNanos(data.expectedDate())) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); } - private static List ofDuration(Duration duration, String inputDate, String zoneId, String expectedDate) { + private static List ofDuration(DurationTestCaseData data) { return List.of( new TestCaseSupplier( - "period, millis; " + zoneId, + "period, millis; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), List.of(DataType.TIME_DURATION, DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData(duration, DataType.TIME_DURATION, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(toMillis(inputDate), DataType.DATETIME, "date") + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "date") ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - equalTo(toMillis(expectedDate)) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) + equalTo(data.expectedDateAsMillis()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( - "period, nanos; " + zoneId, + "period, nanos; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData(duration, DataType.TIME_DURATION, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(toMillis(inputDate)), DataType.DATE_NANOS, "date") + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(data.inputDateAsMillis()), DataType.DATE_NANOS, "date") ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - equalTo(toNanos(expectedDate)) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) + equalTo(toNanos(data.expectedDate())) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); } @@ -181,8 +230,4 @@ private static long toNanos(String timestamp) { protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { return new DateTrunc(source, args.get(0), args.get(1), configuration); } - - private static Configuration configurationForTimezone(String zoneId) { - return new ConfigurationBuilder(randomConfiguration()).query(TEST_SOURCE.text()).zoneId(ZoneId.of(zoneId)).build(); - } } From 24e99c11d0d3410442f8e85db615bffb6b3454d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 31 Oct 2025 13:29:46 +0100 Subject: [PATCH 19/54] Added unit tests for timezones --- .../function/scalar/date/DateTruncTests.java | 123 +++++++++++++++++- 1 file changed, 116 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 9855bf810028b..021262fa87d6d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; import org.elasticsearch.xpack.esql.session.Configuration; +import org.hamcrest.BaseMatcher; import org.hamcrest.Matchers; import java.time.Duration; @@ -29,6 +30,7 @@ import java.util.function.Supplier; import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; +import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER; import static org.hamcrest.Matchers.equalTo; /** @@ -98,7 +100,41 @@ public static List makeTruncDurationTestCases() { /// new DurationTestCaseData(Duration.ofHours(3), ts, "UTC", "2023-02-17T09:00:00Z"), new DurationTestCaseData(Duration.ofHours(3), ts, "-02:00", "2023-02-17T08:00:00Z"), - new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z") + new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "UTC", "2020-01-01T03:00:00Z"), + new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z"), + new DurationTestCaseData(Duration.ofMinutes(3 * 60), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z"), + + // TODO: What to do with hours/minutes/seconds that are not divisors of a full day? + //new DurationTestCaseData(Duration.ofHours(5), "2020-01-01T05:30:00Z", "+01", "2020-01-01T05:00:00Z"), + + /// + /// Daylight savings + /// + //- +1 -> +2 at 2025-03-30T02:00:00+01:00 + new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T00:00:00+01:00", "Europe/Paris", "2025-03-30T00:00:00+01:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T01:00:00+01:00", "Europe/Paris", "2025-03-30T00:00:00+01:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T03:00:00+02:00", "Europe/Paris", "2025-03-30T03:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T04:00:00+02:00", "Europe/Paris", "2025-03-30T03:00:00+02:00"), + // - +2 -> +1 at 2025-10-26T03:00:00+02:00 + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T01:00:00+02:00", "Europe/Paris", "2025-10-26T00:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T02:00:00+02:00", "Europe/Paris", "2025-10-26T00:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T02:00:00+01:00", "Europe/Paris", "2025-10-26T00:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T03:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T04:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), + // Bigger intervals + new DurationTestCaseData(Duration.ofHours(12), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(24), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), + // TODO: What to do with hours over a day? + // new DurationTestCaseData(Duration.ofHours(48), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00") + + /// + /// Partial hours timezones + /// + new DurationTestCaseData(Duration.ofMinutes(1), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T02:09:00+01:15"), + new DurationTestCaseData(Duration.ofMinutes(30), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T02:00:00+01:15"), + new DurationTestCaseData(Duration.ofHours(1), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T02:00:00+01:15"), + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T00:00:00+01:15"), + new DurationTestCaseData(Duration.ofHours(24), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T00:00:00+01:15") ); } @@ -121,9 +157,26 @@ public static List makeTruncPeriodTestCases() { new PeriodTestCaseData(Period.ofYears(5), ts, "UTC", "2021-01-01T00:00:00.00Z"), /// - /// Timezones + /// Timezone with DST (-5 to -4 at 2025-03-09T02:00:00-05, and -4 to -5 at 2025-11-02T02:00:00-04) /// - new PeriodTestCaseData(Period.ofMonths(3), "2023-01-01T00:00:00.00Z", "-01:00", "2022-10-01T01:00:00.00Z") + new PeriodTestCaseData(Period.ofDays(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-09T00:00:00-05:00"), + new PeriodTestCaseData(Period.ofMonths(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-01T00:00:00-05:00"), + new PeriodTestCaseData(Period.ofYears(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-01-01T00:00:00-05:00"), + new PeriodTestCaseData(Period.ofDays(10), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-03T00:00:00-05:00"), + new PeriodTestCaseData(Period.ofDays(1), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-11-02T00:00:00-04:00"), + new PeriodTestCaseData(Period.ofDays(7), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-10-27T00:00:00-04:00"), + new PeriodTestCaseData(Period.ofMonths(3), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-10-01T00:00:00-04:00"), + + /// + /// Partial hours timezones + /// + new PeriodTestCaseData(Period.ofDays(1), "2025-03-09T07:08:09-05:45", "-05:45", "2025-03-09T00:00:00-05:45"), + new PeriodTestCaseData(Period.ofMonths(1), "2025-03-09T07:08:09-05:45", "-05:45", "2025-03-01T00:00:00-05:45"), + new PeriodTestCaseData(Period.ofYears(1), "2025-03-09T07:08:09-05:45", "-05:45", "2025-01-01T00:00:00-05:45"), + new PeriodTestCaseData(Period.ofDays(10), "2025-03-09T07:08:09-05:45", "-05:45", "2025-03-03T00:00:00-05:45"), + new PeriodTestCaseData(Period.ofDays(1), "2025-03-09T07:08:09-05:45", "-05:45", "2025-03-09T00:00:00-05:45"), + new PeriodTestCaseData(Period.ofDays(7), "2025-03-09T07:08:09-05:45", "-05:45", "2025-03-03T00:00:00-05:45"), + new PeriodTestCaseData(Period.ofMonths(3), "2025-03-09T07:08:09-05:45", "-05:45", "2025-01-01T00:00:00-05:45") ); } @@ -139,7 +192,7 @@ private static List ofDatePeriod(PeriodTestCaseData data) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - equalTo(data.expectedDateAsMillis()) + new DateMillisMatcher(data.expectedDate()) // equalTo(data.expectedDateAsMillis()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -152,7 +205,7 @@ private static List ofDatePeriod(PeriodTestCaseData data) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - equalTo(toNanos(data.expectedDate())) + new DateNanosMatcher(data.expectedDate()) // equalTo(toNanos(data.expectedDate())) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); @@ -170,7 +223,7 @@ private static List ofDuration(DurationTestCaseData data) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - equalTo(data.expectedDateAsMillis()) + new DateMillisMatcher(data.expectedDate()) // equalTo(data.expectedDateAsMillis()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -183,7 +236,7 @@ private static List ofDuration(DurationTestCaseData data) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - equalTo(toNanos(data.expectedDate())) + new DateNanosMatcher(data.expectedDate()) // equalTo(toNanos(data.expectedDate())) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); @@ -230,4 +283,60 @@ private static long toNanos(String timestamp) { protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { return new DateTrunc(source, args.get(0), args.get(1), configuration); } + + public static class DateMillisMatcher extends BaseMatcher { + private final long timeMillis; + + public DateMillisMatcher(String date) { + this.timeMillis = toMillis(date); + } + + @Override + public boolean matches(Object item) { + return item instanceof Long && timeMillis == (Long) item; + } + + @Override + public void describeMismatch(Object item, org.hamcrest.Description description) { + description.appendText("was "); + if (item instanceof Long l) { + description.appendValue(DEFAULT_DATE_TIME_FORMATTER.formatMillis(l)); + } else { + description.appendValue(item); + } + } + + @Override + public void describeTo(org.hamcrest.Description description) { + description.appendText(DEFAULT_DATE_TIME_FORMATTER.formatMillis(timeMillis)); + } + } + + public static class DateNanosMatcher extends BaseMatcher { + private final long timeNanos; + + public DateNanosMatcher(String date) { + this.timeNanos = toNanos(date); + } + + @Override + public boolean matches(Object item) { + return item instanceof Long && timeNanos == (Long) item; + } + + @Override + public void describeMismatch(Object item, org.hamcrest.Description description) { + description.appendText("was "); + if (item instanceof Long l) { + description.appendValue(DEFAULT_DATE_TIME_FORMATTER.formatNanos(l)); + } else { + description.appendValue(item); + } + } + + @Override + public void describeTo(org.hamcrest.Description description) { + description.appendText(DEFAULT_DATE_TIME_FORMATTER.formatNanos(timeNanos)); + } + } } From dd523d5a7dcd084987fb03db27b19186d23e305c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 31 Oct 2025 13:41:10 +0100 Subject: [PATCH 20/54] Extracted matchers --- .../function/grouping/BucketTests.java | 10 ++- .../function/grouping/TBucketTests.java | 10 ++- .../function/scalar/date/DateTruncTests.java | 78 ++----------------- .../date/matchers/DateMillisMatcher.java | 48 ++++++++++++ .../date/matchers/DateNanosMatcher.java | 49 ++++++++++++ 5 files changed, 115 insertions(+), 80 deletions(-) create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateMillisMatcher.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateNanosMatcher.java diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java index d6a5a4bcb3d71..966d1643baf8b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java @@ -21,6 +21,8 @@ import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; +import org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers.DateMillisMatcher; +import org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers.DateNanosMatcher; import org.elasticsearch.xpack.esql.session.Configuration; import org.hamcrest.Matcher; import org.hamcrest.Matchers; @@ -152,7 +154,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - equalTo(data.expectedDateAsMillis()) + new DateMillisMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -169,7 +171,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - equalTo(DateUtils.toLong(Instant.parse(data.expectedDate()))) + new DateNanosMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ) @@ -189,7 +191,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - equalTo(data.expectedDateAsMillis()) + new DateMillisMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -206,7 +208,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - equalTo(DateUtils.toLong(Instant.parse(data.expectedDate()))) + new DateNanosMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java index 1f051b4a8afca..05bc8746dc5b6 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java @@ -20,6 +20,8 @@ import org.elasticsearch.xpack.esql.expression.function.DocsV3Support; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; +import org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers.DateMillisMatcher; +import org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers.DateNanosMatcher; import org.elasticsearch.xpack.esql.session.Configuration; import org.hamcrest.Matcher; import org.hamcrest.Matchers; @@ -137,7 +139,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - equalTo(data.expectedDateAsMillis()) + new DateMillisMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -154,7 +156,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - equalTo(DateUtils.toLong(Instant.parse(data.expectedDate()))) + new DateNanosMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ) @@ -174,7 +176,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - equalTo(data.expectedDateAsMillis()) + new DateMillisMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -191,7 +193,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - equalTo(DateUtils.toLong(Instant.parse(data.expectedDate()))) + new DateNanosMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 021262fa87d6d..8cdce9280a85c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -17,6 +17,8 @@ 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.expression.function.scalar.date.matchers.DateMillisMatcher; +import org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers.DateNanosMatcher; import org.elasticsearch.xpack.esql.session.Configuration; import org.hamcrest.BaseMatcher; import org.hamcrest.Matchers; @@ -62,10 +64,6 @@ public ZoneId zoneId() { public long inputDateAsMillis() { return Instant.parse(inputDate).toEpochMilli(); } - - public long expectedDateAsMillis() { - return Instant.parse(expectedDate).toEpochMilli(); - } } public record DurationTestCaseData(Duration duration, String inputDate, @Nullable String zoneIdString, String expectedDate) { @@ -76,10 +74,6 @@ public ZoneId zoneId() { public long inputDateAsMillis() { return Instant.parse(inputDate).toEpochMilli(); } - - public long expectedDateAsMillis() { - return Instant.parse(expectedDate).toEpochMilli(); - } } public static List makeTruncDurationTestCases() { @@ -192,7 +186,7 @@ private static List ofDatePeriod(PeriodTestCaseData data) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - new DateMillisMatcher(data.expectedDate()) // equalTo(data.expectedDateAsMillis()) + new DateMillisMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -205,7 +199,7 @@ private static List ofDatePeriod(PeriodTestCaseData data) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - new DateNanosMatcher(data.expectedDate()) // equalTo(toNanos(data.expectedDate())) + new DateNanosMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); @@ -223,7 +217,7 @@ private static List ofDuration(DurationTestCaseData data) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - new DateMillisMatcher(data.expectedDate()) // equalTo(data.expectedDateAsMillis()) + new DateMillisMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -236,7 +230,7 @@ private static List ofDuration(DurationTestCaseData data) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - new DateNanosMatcher(data.expectedDate()) // equalTo(toNanos(data.expectedDate())) + new DateNanosMatcher(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); @@ -275,68 +269,8 @@ 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 DateTrunc(source, args.get(0), args.get(1), configuration); } - - public static class DateMillisMatcher extends BaseMatcher { - private final long timeMillis; - - public DateMillisMatcher(String date) { - this.timeMillis = toMillis(date); - } - - @Override - public boolean matches(Object item) { - return item instanceof Long && timeMillis == (Long) item; - } - - @Override - public void describeMismatch(Object item, org.hamcrest.Description description) { - description.appendText("was "); - if (item instanceof Long l) { - description.appendValue(DEFAULT_DATE_TIME_FORMATTER.formatMillis(l)); - } else { - description.appendValue(item); - } - } - - @Override - public void describeTo(org.hamcrest.Description description) { - description.appendText(DEFAULT_DATE_TIME_FORMATTER.formatMillis(timeMillis)); - } - } - - public static class DateNanosMatcher extends BaseMatcher { - private final long timeNanos; - - public DateNanosMatcher(String date) { - this.timeNanos = toNanos(date); - } - - @Override - public boolean matches(Object item) { - return item instanceof Long && timeNanos == (Long) item; - } - - @Override - public void describeMismatch(Object item, org.hamcrest.Description description) { - description.appendText("was "); - if (item instanceof Long l) { - description.appendValue(DEFAULT_DATE_TIME_FORMATTER.formatNanos(l)); - } else { - description.appendValue(item); - } - } - - @Override - public void describeTo(org.hamcrest.Description description) { - description.appendText(DEFAULT_DATE_TIME_FORMATTER.formatNanos(timeNanos)); - } - } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateMillisMatcher.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateMillisMatcher.java new file mode 100644 index 0000000000000..9ed90896a7f63 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateMillisMatcher.java @@ -0,0 +1,48 @@ +/* + * 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.matchers; + +import org.hamcrest.BaseMatcher; + +import java.time.Instant; + +import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER; + +/** + * Test matcher for ESQL datetimes that expects longs, but describes the errors as dates, for better readability. + *

+ * See {@link DateNanosMatcher} for the date nanos counterpart. + *

+ */ +public class DateMillisMatcher extends BaseMatcher { + private final long timeMillis; + + public DateMillisMatcher(String date) { + this.timeMillis = Instant.parse(date).toEpochMilli(); + } + + @Override + public boolean matches(Object item) { + return item instanceof Long && timeMillis == (Long) item; + } + + @Override + public void describeMismatch(Object item, org.hamcrest.Description description) { + description.appendText("was "); + if (item instanceof Long l) { + description.appendValue(DEFAULT_DATE_TIME_FORMATTER.formatMillis(l)); + } else { + description.appendValue(item); + } + } + + @Override + public void describeTo(org.hamcrest.Description description) { + description.appendText(DEFAULT_DATE_TIME_FORMATTER.formatMillis(timeMillis)); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateNanosMatcher.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateNanosMatcher.java new file mode 100644 index 0000000000000..c587b6a2547a9 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateNanosMatcher.java @@ -0,0 +1,49 @@ +/* + * 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.matchers; + +import org.elasticsearch.common.time.DateUtils; +import org.hamcrest.BaseMatcher; + +import java.time.Instant; + +import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER; + +/** + * Test matcher for ESQL date nanos that expects longs, but describes the errors as dates, for better readability. + *

+ * See {@link DateMillisMatcher} for the datetime (millis) counterpart. + *

+ */ +public class DateNanosMatcher extends BaseMatcher { + private final long timeNanos; + + public DateNanosMatcher(String date) { + this.timeNanos = DateUtils.toLong(Instant.parse(date)); + } + + @Override + public boolean matches(Object item) { + return item instanceof Long && timeNanos == (Long) item; + } + + @Override + public void describeMismatch(Object item, org.hamcrest.Description description) { + description.appendText("was "); + if (item instanceof Long l) { + description.appendValue(DEFAULT_DATE_TIME_FORMATTER.formatNanos(l)); + } else { + description.appendValue(item); + } + } + + @Override + public void describeTo(org.hamcrest.Description description) { + description.appendText(DEFAULT_DATE_TIME_FORMATTER.formatNanos(timeNanos)); + } +} From ad32a982d8e8c05971ffe9465f275a7053d93457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 31 Oct 2025 13:46:53 +0100 Subject: [PATCH 21/54] Moved matchers to a common place --- .../date => common}/matchers/DateMillisMatcher.java | 2 +- .../date => common}/matchers/DateNanosMatcher.java | 2 +- .../expression/function/grouping/BucketTests.java | 4 ++-- .../expression/function/grouping/TBucketTests.java | 4 ++-- .../function/scalar/date/DateTruncTests.java | 12 +++++------- 5 files changed, 11 insertions(+), 13 deletions(-) rename x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/{expression/function/scalar/date => common}/matchers/DateMillisMatcher.java (94%) rename x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/{expression/function/scalar/date => common}/matchers/DateNanosMatcher.java (95%) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateMillisMatcher.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateMillisMatcher.java similarity index 94% rename from x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateMillisMatcher.java rename to x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateMillisMatcher.java index 9ed90896a7f63..74d80fd3806ec 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateMillisMatcher.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateMillisMatcher.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers; +package org.elasticsearch.xpack.esql.common.matchers; import org.hamcrest.BaseMatcher; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateNanosMatcher.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateNanosMatcher.java similarity index 95% rename from x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateNanosMatcher.java rename to x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateNanosMatcher.java index c587b6a2547a9..38856bd31c1a1 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/matchers/DateNanosMatcher.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateNanosMatcher.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers; +package org.elasticsearch.xpack.esql.common.matchers; import org.elasticsearch.common.time.DateUtils; import org.hamcrest.BaseMatcher; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java index 966d1643baf8b..f2062eeb83239 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java @@ -15,14 +15,14 @@ import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.logging.LogManager; +import org.elasticsearch.xpack.esql.common.matchers.DateMillisMatcher; +import org.elasticsearch.xpack.esql.common.matchers.DateNanosMatcher; 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.EsqlFunctionRegistry; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; -import org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers.DateMillisMatcher; -import org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers.DateNanosMatcher; import org.elasticsearch.xpack.esql.session.Configuration; import org.hamcrest.Matcher; import org.hamcrest.Matchers; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java index 05bc8746dc5b6..043c91009edfe 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java @@ -14,14 +14,14 @@ import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.logging.LogManager; +import org.elasticsearch.xpack.esql.common.matchers.DateMillisMatcher; +import org.elasticsearch.xpack.esql.common.matchers.DateNanosMatcher; 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.DocsV3Support; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; -import org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers.DateMillisMatcher; -import org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers.DateNanosMatcher; import org.elasticsearch.xpack.esql.session.Configuration; import org.hamcrest.Matcher; import org.hamcrest.Matchers; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 8cdce9280a85c..b5a39e75f0bce 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -12,15 +12,14 @@ import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.core.Nullable; +import org.elasticsearch.xpack.esql.common.matchers.DateMillisMatcher; +import org.elasticsearch.xpack.esql.common.matchers.DateNanosMatcher; 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.TestCaseSupplier; import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; -import org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers.DateMillisMatcher; -import org.elasticsearch.xpack.esql.expression.function.scalar.date.matchers.DateNanosMatcher; import org.elasticsearch.xpack.esql.session.Configuration; -import org.hamcrest.BaseMatcher; import org.hamcrest.Matchers; import java.time.Duration; @@ -32,7 +31,6 @@ import java.util.function.Supplier; import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; -import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER; import static org.hamcrest.Matchers.equalTo; /** @@ -99,12 +97,12 @@ public static List makeTruncDurationTestCases() { new DurationTestCaseData(Duration.ofMinutes(3 * 60), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z"), // TODO: What to do with hours/minutes/seconds that are not divisors of a full day? - //new DurationTestCaseData(Duration.ofHours(5), "2020-01-01T05:30:00Z", "+01", "2020-01-01T05:00:00Z"), + // new DurationTestCaseData(Duration.ofHours(5), "2020-01-01T05:30:00Z", "+01", "2020-01-01T05:00:00Z"), /// /// Daylight savings /// - //- +1 -> +2 at 2025-03-30T02:00:00+01:00 + // - +1 -> +2 at 2025-03-30T02:00:00+01:00 new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T00:00:00+01:00", "Europe/Paris", "2025-03-30T00:00:00+01:00"), new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T01:00:00+01:00", "Europe/Paris", "2025-03-30T00:00:00+01:00"), new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T03:00:00+02:00", "Europe/Paris", "2025-03-30T03:00:00+02:00"), @@ -127,7 +125,7 @@ public static List makeTruncDurationTestCases() { new DurationTestCaseData(Duration.ofMinutes(1), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T02:09:00+01:15"), new DurationTestCaseData(Duration.ofMinutes(30), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T02:00:00+01:15"), new DurationTestCaseData(Duration.ofHours(1), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T02:00:00+01:15"), - new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T00:00:00+01:15"), + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T00:00:00+01:15"), new DurationTestCaseData(Duration.ofHours(24), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T00:00:00+01:15") ); } From 539b7a772cef2cfc9d0b62b8f4d27d0efed784df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 31 Oct 2025 14:26:50 +0100 Subject: [PATCH 22/54] Extracted date_trunc csv tests to date --- .../src/main/resources/date.csv-spec | 547 ------------------ .../src/main/resources/date_trunc.csv-spec | 547 ++++++++++++++++++ .../function/scalar/date/DateTrunc.java | 4 +- 3 files changed, 549 insertions(+), 549 deletions(-) create mode 100644 x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_trunc.csv-spec 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 c3bf0d9d0ebbf..92e51dfab858e 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 @@ -86,111 +86,6 @@ min:date | max:date 1985-02-18T00:00:00.000Z | 1999-04-30T00:00:00.000Z ; -evalDateTruncIntervalExpressionPeriod -from employees | sort hire_date | eval x = date_trunc(1 month, hire_date) | keep emp_no, hire_date, x | limit 5; - -emp_no:integer | hire_date:date | x:date -10009 | 1985-02-18T00:00:00.000Z | 1985-02-01T00:00:00.000Z -10048 | 1985-02-24T00:00:00.000Z | 1985-02-01T00:00:00.000Z -10098 | 1985-05-13T00:00:00.000Z | 1985-05-01T00:00:00.000Z -10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z -10061 | 1985-09-17T00:00:00.000Z | 1985-09-01T00:00:00.000Z -; - -evalDateTruncIntervalExpressionDuration -from employees | sort hire_date | eval x = date_trunc(240 hours, hire_date) | keep emp_no, hire_date, x | limit 5; - -emp_no:integer | hire_date:date | x:date -10009 | 1985-02-18T00:00:00.000Z | 1985-02-11T00:00:00.000Z -10048 | 1985-02-24T00:00:00.000Z | 1985-02-21T00:00:00.000Z -10098 | 1985-05-13T00:00:00.000Z | 1985-05-12T00:00:00.000Z -10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z -10061 | 1985-09-17T00:00:00.000Z | 1985-09-09T00:00:00.000Z -; - -evalDateTruncWeeklyInterval -from employees | sort hire_date | eval x = date_trunc(1 week, hire_date) | keep emp_no, hire_date, x | limit 5; - -emp_no:integer | hire_date:date | x:date -10009 | 1985-02-18T00:00:00.000Z | 1985-02-18T00:00:00.000Z -10048 | 1985-02-24T00:00:00.000Z | 1985-02-18T00:00:00.000Z -10098 | 1985-05-13T00:00:00.000Z | 1985-05-13T00:00:00.000Z -10076 | 1985-07-09T00:00:00.000Z | 1985-07-08T00:00:00.000Z -10061 | 1985-09-17T00:00:00.000Z | 1985-09-16T00:00:00.000Z -; - -evalDateTruncQuarterlyInterval -from employees | sort hire_date | eval x = date_trunc(3 month, hire_date) | keep emp_no, hire_date, x | limit 5; - -emp_no:integer | hire_date:date | x:date -10009 | 1985-02-18T00:00:00.000Z | 1985-01-01T00:00:00.000Z -10048 | 1985-02-24T00:00:00.000Z | 1985-01-01T00:00:00.000Z -10098 | 1985-05-13T00:00:00.000Z | 1985-04-01T00:00:00.000Z -10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z -10061 | 1985-09-17T00:00:00.000Z | 1985-07-01T00:00:00.000Z -; - -evalDateTruncNullDate -from employees | where emp_no == 10040 | eval x = date_trunc(1 day, birth_date) | keep emp_no, birth_date, x; - -emp_no:integer | birth_date:date | x:date -10040 | null | null -; - -evalDateTruncGrouping -from employees | eval y = date_trunc(1 year, hire_date) | stats c = count(emp_no) by y | sort y | keep y, c | limit 5; - -y:date | c:long -1985-01-01T00:00:00.000Z | 11 -1986-01-01T00:00:00.000Z | 11 -1987-01-01T00:00:00.000Z | 15 -1988-01-01T00:00:00.000Z | 9 -1989-01-01T00:00:00.000Z | 13 -; - -in -from employees | eval x = date_trunc(1 year, hire_date) | where birth_date not in (x, hire_date) | keep x, hire_date | sort x desc, hire_date | limit 4; - -x:date |hire_date:date -1999-01-01T00:00:00.000Z|1999-04-30T00:00:00.000Z -1997-01-01T00:00:00.000Z|1997-05-19T00:00:00.000Z -1996-01-01T00:00:00.000Z|1996-11-05T00:00:00.000Z -1995-01-01T00:00:00.000Z|1995-01-27T00:00:00.000Z -; - -dateTruncHour - FROM sample_data -| SORT @timestamp ASC -| EVAL t = DATE_TRUNC(1 HOUR, @timestamp) -| KEEP t; - -t:date -2023-10-23T12:00:00 -2023-10-23T12:00:00 -2023-10-23T13:00:00 -2023-10-23T13:00:00 -2023-10-23T13:00:00 -2023-10-23T13:00:00 -2023-10-23T13:00:00 -; - -dateTruncMinute - FROM sample_data -| SORT @timestamp ASC -| EVAL t = DATE_TRUNC(1 MINUTE, @timestamp) -| KEEP t; - -t:date -2023-10-23T12:15:00 -2023-10-23T12:27:00 -2023-10-23T13:33:00 -2023-10-23T13:51:00 -2023-10-23T13:52:00 -2023-10-23T13:53:00 -2023-10-23T13:55:00 -; - - convertFromDatetime from employees | sort emp_no | keep birth_date | eval bd = to_datetime(birth_date) | limit 2; @@ -1153,63 +1048,6 @@ a:integer | df:keyword 1 | 1989-06-02 ; -docsDateTrunc -// tag::docsDateTrunc[] -FROM employees -| KEEP first_name, last_name, hire_date -| EVAL year_hired = DATE_TRUNC(1 year, hire_date) -// end::docsDateTrunc[] -| SORT first_name -| LIMIT 3 -; - -// tag::docsDateTrunc-result[] -first_name:keyword | last_name:keyword | hire_date:date | year_hired:date -Alejandro |McAlpine |1991-06-26T00:00:00.000Z|1991-01-01T00:00:00.000Z -Amabile |Gomatam |1992-11-18T00:00:00.000Z|1992-01-01T00:00:00.000Z -Anneke |Preusig |1989-06-02T00:00:00.000Z|1989-01-01T00:00:00.000Z -// end::docsDateTrunc-result[] -; - -evalDateTruncString -required_capability: string_literal_auto_casting - -ROW a = 1 -| EVAL year_hired = DATE_TRUNC(1 year, "1991-06-26T00:00:00.000Z") -; - -a:integer | year_hired:date -1 | 1991-01-01T00:00:00.000Z -; - -docsDateTruncHistogram -// tag::docsDateTruncHistogram[] -FROM employees -| EVAL year = DATE_TRUNC(1 year, hire_date) -| STATS hires = COUNT(emp_no) BY year -| SORT year -// end::docsDateTruncHistogram[] -; - -// tag::docsDateTruncHistogram-result[] -hires:long | year:date -11 |1985-01-01T00:00:00.000Z -11 |1986-01-01T00:00:00.000Z -15 |1987-01-01T00:00:00.000Z -9 |1988-01-01T00:00:00.000Z -13 |1989-01-01T00:00:00.000Z -12 |1990-01-01T00:00:00.000Z -6 |1991-01-01T00:00:00.000Z -8 |1992-01-01T00:00:00.000Z -3 |1993-01-01T00:00:00.000Z -4 |1994-01-01T00:00:00.000Z -5 |1995-01-01T00:00:00.000Z -1 |1996-01-01T00:00:00.000Z -1 |1997-01-01T00:00:00.000Z -1 |1999-01-01T00:00:00.000Z -// end::docsDateTruncHistogram-result[] -; - docsNow // tag::docsNow[] ROW current_date = NOW() @@ -1461,99 +1299,6 @@ a:datetime 1958-02-19T00:00:00 ; -evalDateTruncMonthInString -required_capability: implicit_casting_string_literal_to_temporal_amount - -FROM employees -| SORT hire_date -| EVAL x = date_trunc("1 month", hire_date) -| KEEP emp_no, hire_date, x -| LIMIT 5; - -emp_no:integer | hire_date:date | x:date -10009 | 1985-02-18T00:00:00.000Z | 1985-02-01T00:00:00.000Z -10048 | 1985-02-24T00:00:00.000Z | 1985-02-01T00:00:00.000Z -10098 | 1985-05-13T00:00:00.000Z | 1985-05-01T00:00:00.000Z -10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z -10061 | 1985-09-17T00:00:00.000Z | 1985-09-01T00:00:00.000Z -; - -evalDateTruncHourInString -required_capability: implicit_casting_string_literal_to_temporal_amount - -FROM employees -| SORT hire_date -| EVAL x = date_trunc("240 hours", hire_date) -| KEEP emp_no, hire_date, x -| LIMIT 5; - -emp_no:integer | hire_date:date | x:date -10009 | 1985-02-18T00:00:00.000Z | 1985-02-11T00:00:00.000Z -10048 | 1985-02-24T00:00:00.000Z | 1985-02-21T00:00:00.000Z -10098 | 1985-05-13T00:00:00.000Z | 1985-05-12T00:00:00.000Z -10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z -10061 | 1985-09-17T00:00:00.000Z | 1985-09-09T00:00:00.000Z -; - -evalDateTruncDayInString -required_capability: implicit_casting_string_literal_to_temporal_amount - -FROM sample_data -| SORT @timestamp ASC -| EVAL t = DATE_TRUNC("1 day", @timestamp) -| KEEP t; - -t:date -2023-10-23T00:00:00 -2023-10-23T00:00:00 -2023-10-23T00:00:00 -2023-10-23T00:00:00 -2023-10-23T00:00:00 -2023-10-23T00:00:00 -2023-10-23T00:00:00 -; - -evalDateTruncMinuteInString -required_capability: implicit_casting_string_literal_to_temporal_amount - -FROM sample_data -| SORT @timestamp ASC -| EVAL t = DATE_TRUNC("1 minute", @timestamp) -| KEEP t; - -t:date -2023-10-23T12:15:00 -2023-10-23T12:27:00 -2023-10-23T13:33:00 -2023-10-23T13:51:00 -2023-10-23T13:52:00 -2023-10-23T13:53:00 -2023-10-23T13:55:00 -; - -evalDateTruncDayInStringNull -required_capability: implicit_casting_string_literal_to_temporal_amount - -FROM employees -| WHERE emp_no == 10040 -| EVAL x = date_trunc("1 day", birth_date) -| KEEP emp_no, birth_date, x; - -emp_no:integer | birth_date:date | x:date -10040 | null | null -; - -evalDateTruncYearInString -required_capability: implicit_casting_string_literal_to_temporal_amount - -ROW a = 1 -| EVAL year_hired = DATE_TRUNC("1 year", "1991-06-26T00:00:00.000Z") -; - -a:integer | year_hired:date -1 | 1991-01-01T00:00:00.000Z -; - filteringWithTemporalAmountInString required_capability: implicit_casting_string_literal_to_temporal_amount @@ -1611,298 +1356,6 @@ result:boolean null ; -evalDateTruncYearInArbitraryIntervals -required_capability: date_trunc_with_arbitrary_intervals - -ROW x = ["1963-01-01", "1973-04-11", "1978-04-12", "0000-01-01", "-0006-01-01", "-0007-01-01"]::DATETIME -| MV_EXPAND x -| EVAL y = DATE_TRUNC(7 years, x) -; - -x:date | y:date -1963-01-01T00:00:00.000Z | 1961-01-01T00:00:00.000Z -1973-04-11T00:00:00.000Z | 1968-01-01T00:00:00.000Z -1978-04-12T00:00:00.000Z | 1975-01-01T00:00:00.000Z -0000-01-01T00:00:00.000Z | -0006-01-01T00:00:00.000Z --0006-01-01T00:00:00.000Z | -0006-01-01T00:00:00.000Z --0007-01-01T00:00:00.000Z | -0013-01-01T00:00:00.000Z -; - -evalDateTruncMonthInArbitraryIntervals -required_capability: date_trunc_with_arbitrary_intervals - -ROW x = ["1969-11-12", "1970-05-01", "1970-12-31", "1972-01-12", "0001-01-01", "0000-12-01", "-0001-12-01"]::DATETIME -| MV_EXPAND x -| EVAL y = DATE_TRUNC(7 months, x) -; - -x:date | y:date -1969-11-12T00:00:00.000Z | 1969-10-01T00:00:00.000Z -1970-05-01T00:00:00.000Z | 1970-05-01T00:00:00.000Z -1970-12-31T00:00:00.000Z | 1970-12-01T00:00:00.000Z -1972-01-12T00:00:00.000Z | 1971-07-01T00:00:00.000Z -0001-01-01T00:00:00.000Z | 0001-01-01T00:00:00.000Z -0000-12-01T00:00:00.000Z | 0000-06-01T00:00:00.000Z --0001-12-01T00:00:00.000Z | -0001-11-01T00:00:00.000Z - -; - -evalDateTruncMonthIntervalWithGTEInRange -FROM employees -| SORT hire_date -| WHERE hire_date >= "1990-01-01" -| EVAL x = date_trunc(1 month, hire_date) -| KEEP emp_no, hire_date, x -| LIMIT 5; - -emp_no:integer | hire_date:date | x:date -10082 | 1990-01-03T00:00:00.000Z | 1990-01-01T00:00:00.000Z -10096 | 1990-01-14T00:00:00.000Z | 1990-01-01T00:00:00.000Z -10011 | 1990-01-22T00:00:00.000Z | 1990-01-01T00:00:00.000Z -10056 | 1990-02-01T00:00:00.000Z | 1990-02-01T00:00:00.000Z -10086 | 1990-02-16T00:00:00.000Z | 1990-02-01T00:00:00.000Z -; - -evalDateTruncHoursIntervalWithLTEInRange -FROM employees -| SORT hire_date desc -| WHERE hire_date <= "1990-01-01" -| EVAL x = date_trunc(240 hours, hire_date) -| KEEP emp_no, hire_date, x -| LIMIT 5; - -emp_no:integer | hire_date:date | x:date -10023 | 1989-12-17T00:00:00.000Z | 1989-12-17T00:00:00.000Z -10041 | 1989-11-12T00:00:00.000Z | 1989-11-07T00:00:00.000Z -10069 | 1989-11-05T00:00:00.000Z | 1989-10-28T00:00:00.000Z -10092 | 1989-09-22T00:00:00.000Z | 1989-09-18T00:00:00.000Z -10038 | 1989-09-20T00:00:00.000Z | 1989-09-18T00:00:00.000Z -; - -evalDateTruncWeeklyIntervalWithLTGTInRange -from employees -| SORT hire_date -| WHERE hire_date > "1986-01-01" and hire_date < "1988-01-01" -| EVAL x = date_trunc(1 week, hire_date) -| KEEP emp_no, hire_date, x -| LIMIT 5; - -emp_no:integer | hire_date:date | x:date -10053 | 1986-02-04T00:00:00.000Z | 1986-02-03T00:00:00.000Z -10066 | 1986-02-26T00:00:00.000Z | 1986-02-24T00:00:00.000Z -10090 | 1986-03-14T00:00:00.000Z | 1986-03-10T00:00:00.000Z -10079 | 1986-03-27T00:00:00.000Z | 1986-03-24T00:00:00.000Z -10001 | 1986-06-26T00:00:00.000Z | 1986-06-23T00:00:00.000Z -; - -evalDateTruncQuarterlyIntervalWithGTInRange -from employees -| SORT hire_date -| WHERE hire_date > "1980-01-01" -| EVAL x = date_trunc(3 month, hire_date) -| KEEP emp_no, hire_date, x -| LIMIT 5; - -emp_no:integer | hire_date:date | x:date -10009 | 1985-02-18T00:00:00.000Z | 1985-01-01T00:00:00.000Z -10048 | 1985-02-24T00:00:00.000Z | 1985-01-01T00:00:00.000Z -10098 | 1985-05-13T00:00:00.000Z | 1985-04-01T00:00:00.000Z -10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z -10061 | 1985-09-17T00:00:00.000Z | 1985-07-01T00:00:00.000Z -; - -dateTruncGroupingYearIntervalWithLTInRange -from employees -| WHERE hire_date < "2025-01-01" -| EVAL y = date_trunc(1 year, hire_date) -| stats c = count(emp_no) by y -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -1985-01-01T00:00:00.000Z | 11 -1986-01-01T00:00:00.000Z | 11 -1987-01-01T00:00:00.000Z | 15 -1988-01-01T00:00:00.000Z | 9 -1989-01-01T00:00:00.000Z | 13 -; - -dateTruncGroupingYearIntervalWithLTOutOfRange -from employees -| WHERE hire_date < "1980-01-01" -| EVAL y = date_trunc(1 year, hire_date) -| stats c = count(emp_no) by y -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -; - -dateTruncGroupingYearIntervalWithGTOutOfRange -from employees -| WHERE hire_date > "2000-01-01" -| EVAL y = date_trunc(1 year, hire_date) -| stats c = count(emp_no) by y -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -; - -dateTruncGroupingMonthIntervalWithLTGTInRange -from employees -| WHERE hire_date > "1987-01-01" and hire_date < "1988-01-01" -| EVAL y = date_trunc(1 month, hire_date) -| stats c = count(emp_no) by y -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -1987-03-01T00:00:00.000Z | 5 -1987-04-01T00:00:00.000Z | 3 -1987-05-01T00:00:00.000Z | 1 -1987-07-01T00:00:00.000Z | 1 -1987-08-01T00:00:00.000Z | 2 -; - -dateTruncGroupingDayIntervalWithEQInRange -from employees -| WHERE hire_date == "1988-02-10" -| EVAL y = date_trunc(1 day, hire_date) -| stats c = count(emp_no) by y -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -1988-02-10T00:00:00.000Z | 1 -; - -dateTruncGroupingDayIntervalWithEQOutOfRange -from employees -| WHERE hire_date == "2025-01-01" -| EVAL y = date_trunc(1 day, hire_date) -| stats c = count(emp_no) by y -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -; - -dateTruncWithEval -from employees -| EVAL x = hire_date -| WHERE x > "1987-01-01" and hire_date < "1988-01-01" -| EVAL y = date_trunc(1 month, x) -| STATS c = count(emp_no) by y -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -1987-03-01T00:00:00.000Z | 5 -1987-04-01T00:00:00.000Z | 3 -1987-05-01T00:00:00.000Z | 1 -1987-07-01T00:00:00.000Z | 1 -1987-08-01T00:00:00.000Z | 2 -; - -dateTruncWithEvalExpression -from employees -| EVAL x = hire_date + 1 year -| WHERE x > "1987-01-01" and x < "1988-01-01" -| EVAL y = date_trunc(1 month, x) -| STATS c = count(emp_no) by y -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -1987-02-01T00:00:00.000Z | 2 -1987-03-01T00:00:00.000Z | 2 -1987-06-01T00:00:00.000Z | 1 -1987-07-01T00:00:00.000Z | 1 -1987-08-01T00:00:00.000Z | 2 -; - -dateTruncWithRename -FROM employees -| RENAME hire_date as x -| WHERE x > "1987-01-01" and x < "1988-01-01" -| EVAL y = date_trunc(1 month, x) -| STATS c = count(emp_no) by y -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -1987-03-01T00:00:00.000Z | 5 -1987-04-01T00:00:00.000Z | 3 -1987-05-01T00:00:00.000Z | 1 -1987-07-01T00:00:00.000Z | 1 -1987-08-01T00:00:00.000Z | 2 -; - -dateTruncWithRenameChain -FROM employees -| RENAME hire_date as a, a as x -| WHERE x > "1987-01-01" and x < "1988-01-01" -| EVAL y = date_trunc(1 month, x) -| STATS c = count(emp_no) by y -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -1987-03-01T00:00:00.000Z | 5 -1987-04-01T00:00:00.000Z | 3 -1987-05-01T00:00:00.000Z | 1 -1987-07-01T00:00:00.000Z | 1 -1987-08-01T00:00:00.000Z | 2 -; - -dateTruncWithRenameBack -FROM employees -| RENAME hire_date as x, x as hire_date -| WHERE hire_date > "1987-01-01" and hire_date < "1988-01-01" -| EVAL y = date_trunc(1 month, hire_date) -| STATS c = count(emp_no) by y -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -1987-03-01T00:00:00.000Z | 5 -1987-04-01T00:00:00.000Z | 3 -1987-05-01T00:00:00.000Z | 1 -1987-07-01T00:00:00.000Z | 1 -1987-08-01T00:00:00.000Z | 2 -; - -dateTruncWithEvalRename -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 -| SORT y -| KEEP y, c -| LIMIT 5; - -y:date | c:long -1987-03-01T00:00:00.000Z | 5 -1987-04-01T00:00:00.000Z | 3 -1987-05-01T00:00:00.000Z | 1 -1987-07-01T00:00:00.000Z | 1 -1987-08-01T00:00:00.000Z | 2 -; - dayNameRowTest required_capability:fn_day_name // tag::docsDayName[] diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_trunc.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_trunc.csv-spec new file mode 100644 index 0000000000000..a65285b8ab340 --- /dev/null +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_trunc.csv-spec @@ -0,0 +1,547 @@ +evalDateTruncIntervalExpressionPeriod +from employees | sort hire_date | eval x = date_trunc(1 month, hire_date) | keep emp_no, hire_date, x | limit 5; + +emp_no:integer | hire_date:date | x:date +10009 | 1985-02-18T00:00:00.000Z | 1985-02-01T00:00:00.000Z +10048 | 1985-02-24T00:00:00.000Z | 1985-02-01T00:00:00.000Z +10098 | 1985-05-13T00:00:00.000Z | 1985-05-01T00:00:00.000Z +10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z +10061 | 1985-09-17T00:00:00.000Z | 1985-09-01T00:00:00.000Z +; + +evalDateTruncIntervalExpressionDuration +from employees | sort hire_date | eval x = date_trunc(240 hours, hire_date) | keep emp_no, hire_date, x | limit 5; + +emp_no:integer | hire_date:date | x:date +10009 | 1985-02-18T00:00:00.000Z | 1985-02-11T00:00:00.000Z +10048 | 1985-02-24T00:00:00.000Z | 1985-02-21T00:00:00.000Z +10098 | 1985-05-13T00:00:00.000Z | 1985-05-12T00:00:00.000Z +10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z +10061 | 1985-09-17T00:00:00.000Z | 1985-09-09T00:00:00.000Z +; + +evalDateTruncWeeklyInterval +from employees | sort hire_date | eval x = date_trunc(1 week, hire_date) | keep emp_no, hire_date, x | limit 5; + +emp_no:integer | hire_date:date | x:date +10009 | 1985-02-18T00:00:00.000Z | 1985-02-18T00:00:00.000Z +10048 | 1985-02-24T00:00:00.000Z | 1985-02-18T00:00:00.000Z +10098 | 1985-05-13T00:00:00.000Z | 1985-05-13T00:00:00.000Z +10076 | 1985-07-09T00:00:00.000Z | 1985-07-08T00:00:00.000Z +10061 | 1985-09-17T00:00:00.000Z | 1985-09-16T00:00:00.000Z +; + +evalDateTruncQuarterlyInterval +from employees | sort hire_date | eval x = date_trunc(3 month, hire_date) | keep emp_no, hire_date, x | limit 5; + +emp_no:integer | hire_date:date | x:date +10009 | 1985-02-18T00:00:00.000Z | 1985-01-01T00:00:00.000Z +10048 | 1985-02-24T00:00:00.000Z | 1985-01-01T00:00:00.000Z +10098 | 1985-05-13T00:00:00.000Z | 1985-04-01T00:00:00.000Z +10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z +10061 | 1985-09-17T00:00:00.000Z | 1985-07-01T00:00:00.000Z +; + +evalDateTruncNullDate +from employees | where emp_no == 10040 | eval x = date_trunc(1 day, birth_date) | keep emp_no, birth_date, x; + +emp_no:integer | birth_date:date | x:date +10040 | null | null +; + +evalDateTruncGrouping +from employees | eval y = date_trunc(1 year, hire_date) | stats c = count(emp_no) by y | sort y | keep y, c | limit 5; + +y:date | c:long +1985-01-01T00:00:00.000Z | 11 +1986-01-01T00:00:00.000Z | 11 +1987-01-01T00:00:00.000Z | 15 +1988-01-01T00:00:00.000Z | 9 +1989-01-01T00:00:00.000Z | 13 +; + +in +from employees | eval x = date_trunc(1 year, hire_date) | where birth_date not in (x, hire_date) | keep x, hire_date | sort x desc, hire_date | limit 4; + +x:date |hire_date:date +1999-01-01T00:00:00.000Z|1999-04-30T00:00:00.000Z +1997-01-01T00:00:00.000Z|1997-05-19T00:00:00.000Z +1996-01-01T00:00:00.000Z|1996-11-05T00:00:00.000Z +1995-01-01T00:00:00.000Z|1995-01-27T00:00:00.000Z +; + +dateTruncHour + FROM sample_data +| SORT @timestamp ASC +| EVAL t = DATE_TRUNC(1 HOUR, @timestamp) +| KEEP t; + +t:date +2023-10-23T12:00:00 +2023-10-23T12:00:00 +2023-10-23T13:00:00 +2023-10-23T13:00:00 +2023-10-23T13:00:00 +2023-10-23T13:00:00 +2023-10-23T13:00:00 +; + +dateTruncMinute + FROM sample_data +| SORT @timestamp ASC +| EVAL t = DATE_TRUNC(1 MINUTE, @timestamp) +| KEEP t; + +t:date +2023-10-23T12:15:00 +2023-10-23T12:27:00 +2023-10-23T13:33:00 +2023-10-23T13:51:00 +2023-10-23T13:52:00 +2023-10-23T13:53:00 +2023-10-23T13:55:00 +; + +docsDateTrunc +// tag::docsDateTrunc[] +FROM employees +| KEEP first_name, last_name, hire_date +| EVAL year_hired = DATE_TRUNC(1 year, hire_date) +// end::docsDateTrunc[] +| SORT first_name +| LIMIT 3 +; + +// tag::docsDateTrunc-result[] +first_name:keyword | last_name:keyword | hire_date:date | year_hired:date +Alejandro |McAlpine |1991-06-26T00:00:00.000Z|1991-01-01T00:00:00.000Z +Amabile |Gomatam |1992-11-18T00:00:00.000Z|1992-01-01T00:00:00.000Z +Anneke |Preusig |1989-06-02T00:00:00.000Z|1989-01-01T00:00:00.000Z +// end::docsDateTrunc-result[] +; + +evalDateTruncString +required_capability: string_literal_auto_casting + +ROW a = 1 +| EVAL year_hired = DATE_TRUNC(1 year, "1991-06-26T00:00:00.000Z") +; + +a:integer | year_hired:date +1 | 1991-01-01T00:00:00.000Z +; + +docsDateTruncHistogram +// tag::docsDateTruncHistogram[] +FROM employees +| EVAL year = DATE_TRUNC(1 year, hire_date) +| STATS hires = COUNT(emp_no) BY year +| SORT year +// end::docsDateTruncHistogram[] +; + +// tag::docsDateTruncHistogram-result[] +hires:long | year:date +11 |1985-01-01T00:00:00.000Z +11 |1986-01-01T00:00:00.000Z +15 |1987-01-01T00:00:00.000Z +9 |1988-01-01T00:00:00.000Z +13 |1989-01-01T00:00:00.000Z +12 |1990-01-01T00:00:00.000Z +6 |1991-01-01T00:00:00.000Z +8 |1992-01-01T00:00:00.000Z +3 |1993-01-01T00:00:00.000Z +4 |1994-01-01T00:00:00.000Z +5 |1995-01-01T00:00:00.000Z +1 |1996-01-01T00:00:00.000Z +1 |1997-01-01T00:00:00.000Z +1 |1999-01-01T00:00:00.000Z +// end::docsDateTruncHistogram-result[] +; + +evalDateTruncYearInArbitraryIntervals +required_capability: date_trunc_with_arbitrary_intervals + +ROW x = ["1963-01-01", "1973-04-11", "1978-04-12", "0000-01-01", "-0006-01-01", "-0007-01-01"]::DATETIME +| MV_EXPAND x +| EVAL y = DATE_TRUNC(7 years, x) +; + +x:date | y:date +1963-01-01T00:00:00.000Z | 1961-01-01T00:00:00.000Z +1973-04-11T00:00:00.000Z | 1968-01-01T00:00:00.000Z +1978-04-12T00:00:00.000Z | 1975-01-01T00:00:00.000Z +0000-01-01T00:00:00.000Z | -0006-01-01T00:00:00.000Z +-0006-01-01T00:00:00.000Z | -0006-01-01T00:00:00.000Z +-0007-01-01T00:00:00.000Z | -0013-01-01T00:00:00.000Z +; + +evalDateTruncMonthInArbitraryIntervals +required_capability: date_trunc_with_arbitrary_intervals + +ROW x = ["1969-11-12", "1970-05-01", "1970-12-31", "1972-01-12", "0001-01-01", "0000-12-01", "-0001-12-01"]::DATETIME +| MV_EXPAND x +| EVAL y = DATE_TRUNC(7 months, x) +; + +x:date | y:date +1969-11-12T00:00:00.000Z | 1969-10-01T00:00:00.000Z +1970-05-01T00:00:00.000Z | 1970-05-01T00:00:00.000Z +1970-12-31T00:00:00.000Z | 1970-12-01T00:00:00.000Z +1972-01-12T00:00:00.000Z | 1971-07-01T00:00:00.000Z +0001-01-01T00:00:00.000Z | 0001-01-01T00:00:00.000Z +0000-12-01T00:00:00.000Z | 0000-06-01T00:00:00.000Z +-0001-12-01T00:00:00.000Z | -0001-11-01T00:00:00.000Z + +; + +evalDateTruncMonthIntervalWithGTEInRange +FROM employees +| SORT hire_date +| WHERE hire_date >= "1990-01-01" +| EVAL x = date_trunc(1 month, hire_date) +| KEEP emp_no, hire_date, x +| LIMIT 5; + +emp_no:integer | hire_date:date | x:date +10082 | 1990-01-03T00:00:00.000Z | 1990-01-01T00:00:00.000Z +10096 | 1990-01-14T00:00:00.000Z | 1990-01-01T00:00:00.000Z +10011 | 1990-01-22T00:00:00.000Z | 1990-01-01T00:00:00.000Z +10056 | 1990-02-01T00:00:00.000Z | 1990-02-01T00:00:00.000Z +10086 | 1990-02-16T00:00:00.000Z | 1990-02-01T00:00:00.000Z +; + +evalDateTruncHoursIntervalWithLTEInRange +FROM employees +| SORT hire_date desc +| WHERE hire_date <= "1990-01-01" +| EVAL x = date_trunc(240 hours, hire_date) +| KEEP emp_no, hire_date, x +| LIMIT 5; + +emp_no:integer | hire_date:date | x:date +10023 | 1989-12-17T00:00:00.000Z | 1989-12-17T00:00:00.000Z +10041 | 1989-11-12T00:00:00.000Z | 1989-11-07T00:00:00.000Z +10069 | 1989-11-05T00:00:00.000Z | 1989-10-28T00:00:00.000Z +10092 | 1989-09-22T00:00:00.000Z | 1989-09-18T00:00:00.000Z +10038 | 1989-09-20T00:00:00.000Z | 1989-09-18T00:00:00.000Z +; + +evalDateTruncWeeklyIntervalWithLTGTInRange +from employees +| SORT hire_date +| WHERE hire_date > "1986-01-01" and hire_date < "1988-01-01" +| EVAL x = date_trunc(1 week, hire_date) +| KEEP emp_no, hire_date, x +| LIMIT 5; + +emp_no:integer | hire_date:date | x:date +10053 | 1986-02-04T00:00:00.000Z | 1986-02-03T00:00:00.000Z +10066 | 1986-02-26T00:00:00.000Z | 1986-02-24T00:00:00.000Z +10090 | 1986-03-14T00:00:00.000Z | 1986-03-10T00:00:00.000Z +10079 | 1986-03-27T00:00:00.000Z | 1986-03-24T00:00:00.000Z +10001 | 1986-06-26T00:00:00.000Z | 1986-06-23T00:00:00.000Z +; + +evalDateTruncQuarterlyIntervalWithGTInRange +from employees +| SORT hire_date +| WHERE hire_date > "1980-01-01" +| EVAL x = date_trunc(3 month, hire_date) +| KEEP emp_no, hire_date, x +| LIMIT 5; + +emp_no:integer | hire_date:date | x:date +10009 | 1985-02-18T00:00:00.000Z | 1985-01-01T00:00:00.000Z +10048 | 1985-02-24T00:00:00.000Z | 1985-01-01T00:00:00.000Z +10098 | 1985-05-13T00:00:00.000Z | 1985-04-01T00:00:00.000Z +10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z +10061 | 1985-09-17T00:00:00.000Z | 1985-07-01T00:00:00.000Z +; + +dateTruncGroupingYearIntervalWithLTInRange +from employees +| WHERE hire_date < "2025-01-01" +| EVAL y = date_trunc(1 year, hire_date) +| stats c = count(emp_no) by y +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +1985-01-01T00:00:00.000Z | 11 +1986-01-01T00:00:00.000Z | 11 +1987-01-01T00:00:00.000Z | 15 +1988-01-01T00:00:00.000Z | 9 +1989-01-01T00:00:00.000Z | 13 +; + +dateTruncGroupingYearIntervalWithLTOutOfRange +from employees +| WHERE hire_date < "1980-01-01" +| EVAL y = date_trunc(1 year, hire_date) +| stats c = count(emp_no) by y +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +; + +dateTruncGroupingYearIntervalWithGTOutOfRange +from employees +| WHERE hire_date > "2000-01-01" +| EVAL y = date_trunc(1 year, hire_date) +| stats c = count(emp_no) by y +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +; + +dateTruncGroupingMonthIntervalWithLTGTInRange +from employees +| WHERE hire_date > "1987-01-01" and hire_date < "1988-01-01" +| EVAL y = date_trunc(1 month, hire_date) +| stats c = count(emp_no) by y +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +1987-03-01T00:00:00.000Z | 5 +1987-04-01T00:00:00.000Z | 3 +1987-05-01T00:00:00.000Z | 1 +1987-07-01T00:00:00.000Z | 1 +1987-08-01T00:00:00.000Z | 2 +; + +dateTruncGroupingDayIntervalWithEQInRange +from employees +| WHERE hire_date == "1988-02-10" +| EVAL y = date_trunc(1 day, hire_date) +| stats c = count(emp_no) by y +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +1988-02-10T00:00:00.000Z | 1 +; + +dateTruncGroupingDayIntervalWithEQOutOfRange +from employees +| WHERE hire_date == "2025-01-01" +| EVAL y = date_trunc(1 day, hire_date) +| stats c = count(emp_no) by y +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +; + +dateTruncWithEval +from employees +| EVAL x = hire_date +| WHERE x > "1987-01-01" and hire_date < "1988-01-01" +| EVAL y = date_trunc(1 month, x) +| STATS c = count(emp_no) by y +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +1987-03-01T00:00:00.000Z | 5 +1987-04-01T00:00:00.000Z | 3 +1987-05-01T00:00:00.000Z | 1 +1987-07-01T00:00:00.000Z | 1 +1987-08-01T00:00:00.000Z | 2 +; + +dateTruncWithEvalExpression +from employees +| EVAL x = hire_date + 1 year +| WHERE x > "1987-01-01" and x < "1988-01-01" +| EVAL y = date_trunc(1 month, x) +| STATS c = count(emp_no) by y +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +1987-02-01T00:00:00.000Z | 2 +1987-03-01T00:00:00.000Z | 2 +1987-06-01T00:00:00.000Z | 1 +1987-07-01T00:00:00.000Z | 1 +1987-08-01T00:00:00.000Z | 2 +; + +dateTruncWithRename +FROM employees +| RENAME hire_date as x +| WHERE x > "1987-01-01" and x < "1988-01-01" +| EVAL y = date_trunc(1 month, x) +| STATS c = count(emp_no) by y +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +1987-03-01T00:00:00.000Z | 5 +1987-04-01T00:00:00.000Z | 3 +1987-05-01T00:00:00.000Z | 1 +1987-07-01T00:00:00.000Z | 1 +1987-08-01T00:00:00.000Z | 2 +; + +dateTruncWithRenameChain +FROM employees +| RENAME hire_date as a, a as x +| WHERE x > "1987-01-01" and x < "1988-01-01" +| EVAL y = date_trunc(1 month, x) +| STATS c = count(emp_no) by y +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +1987-03-01T00:00:00.000Z | 5 +1987-04-01T00:00:00.000Z | 3 +1987-05-01T00:00:00.000Z | 1 +1987-07-01T00:00:00.000Z | 1 +1987-08-01T00:00:00.000Z | 2 +; + +dateTruncWithRenameBack +FROM employees +| RENAME hire_date as x, x as hire_date +| WHERE hire_date > "1987-01-01" and hire_date < "1988-01-01" +| EVAL y = date_trunc(1 month, hire_date) +| STATS c = count(emp_no) by y +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +1987-03-01T00:00:00.000Z | 5 +1987-04-01T00:00:00.000Z | 3 +1987-05-01T00:00:00.000Z | 1 +1987-07-01T00:00:00.000Z | 1 +1987-08-01T00:00:00.000Z | 2 +; + +dateTruncWithEvalRename +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 +| SORT y +| KEEP y, c +| LIMIT 5; + +y:date | c:long +1987-03-01T00:00:00.000Z | 5 +1987-04-01T00:00:00.000Z | 3 +1987-05-01T00:00:00.000Z | 1 +1987-07-01T00:00:00.000Z | 1 +1987-08-01T00:00:00.000Z | 2 +; + + + +evalDateTruncMonthInString +required_capability: implicit_casting_string_literal_to_temporal_amount + +FROM employees +| SORT hire_date +| EVAL x = date_trunc("1 month", hire_date) +| KEEP emp_no, hire_date, x +| LIMIT 5; + +emp_no:integer | hire_date:date | x:date +10009 | 1985-02-18T00:00:00.000Z | 1985-02-01T00:00:00.000Z +10048 | 1985-02-24T00:00:00.000Z | 1985-02-01T00:00:00.000Z +10098 | 1985-05-13T00:00:00.000Z | 1985-05-01T00:00:00.000Z +10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z +10061 | 1985-09-17T00:00:00.000Z | 1985-09-01T00:00:00.000Z +; + +evalDateTruncHourInString +required_capability: implicit_casting_string_literal_to_temporal_amount + +FROM employees +| SORT hire_date +| EVAL x = date_trunc("240 hours", hire_date) +| KEEP emp_no, hire_date, x +| LIMIT 5; + +emp_no:integer | hire_date:date | x:date +10009 | 1985-02-18T00:00:00.000Z | 1985-02-11T00:00:00.000Z +10048 | 1985-02-24T00:00:00.000Z | 1985-02-21T00:00:00.000Z +10098 | 1985-05-13T00:00:00.000Z | 1985-05-12T00:00:00.000Z +10076 | 1985-07-09T00:00:00.000Z | 1985-07-01T00:00:00.000Z +10061 | 1985-09-17T00:00:00.000Z | 1985-09-09T00:00:00.000Z +; + +evalDateTruncDayInString +required_capability: implicit_casting_string_literal_to_temporal_amount + +FROM sample_data +| SORT @timestamp ASC +| EVAL t = DATE_TRUNC("1 day", @timestamp) +| KEEP t; + +t:date +2023-10-23T00:00:00 +2023-10-23T00:00:00 +2023-10-23T00:00:00 +2023-10-23T00:00:00 +2023-10-23T00:00:00 +2023-10-23T00:00:00 +2023-10-23T00:00:00 +; + +evalDateTruncMinuteInString +required_capability: implicit_casting_string_literal_to_temporal_amount + +FROM sample_data +| SORT @timestamp ASC +| EVAL t = DATE_TRUNC("1 minute", @timestamp) +| KEEP t; + +t:date +2023-10-23T12:15:00 +2023-10-23T12:27:00 +2023-10-23T13:33:00 +2023-10-23T13:51:00 +2023-10-23T13:52:00 +2023-10-23T13:53:00 +2023-10-23T13:55:00 +; + +evalDateTruncDayInStringNull +required_capability: implicit_casting_string_literal_to_temporal_amount + +FROM employees +| WHERE emp_no == 10040 +| EVAL x = date_trunc("1 day", birth_date) +| KEEP emp_no, birth_date, x; + +emp_no:integer | birth_date:date | x:date +10040 | null | null +; + +evalDateTruncYearInString +required_capability: implicit_casting_string_literal_to_temporal_amount + +ROW a = 1 +| EVAL year_hired = DATE_TRUNC("1 year", "1991-06-26T00:00:00.000Z") +; + +a:integer | year_hired:date +1 | 1991-01-01T00:00:00.000Z +; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java index 48feb609b10e8..6990aac4f4e20 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java @@ -64,12 +64,12 @@ public interface DateTruncFactoryProvider { returnType = { "date", "date_nanos" }, description = "Rounds down a date to the closest interval since epoch, which starts at `0001-01-01T00:00:00Z`.", examples = { - @Example(file = "date", tag = "docsDateTrunc"), + @Example(file = "date_trunc", tag = "docsDateTrunc"), @Example( description = "Combine `DATE_TRUNC` with [`STATS`](/reference/query-languages/esql/commands/stats-by.md) " + "to create date histograms. " + "For example, the number of hires per year:", - file = "date", + file = "date_trunc", tag = "docsDateTruncHistogram" ), @Example(description = "Or an hourly error rate:", file = "conditional", tag = "docsCaseHourlyErrorRate") } From 5053983744ba296245079c4ea901d7c577a13dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 31 Oct 2025 14:30:30 +0100 Subject: [PATCH 23/54] Update docs/changelog/137450.yaml --- docs/changelog/137450.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/137450.yaml diff --git a/docs/changelog/137450.yaml b/docs/changelog/137450.yaml new file mode 100644 index 0000000000000..d0c9d84bf6b61 --- /dev/null +++ b/docs/changelog/137450.yaml @@ -0,0 +1,5 @@ +pr: 137450 +summary: "Timezone support in DATE_TRUNC, BUCKET and TBUCKET" +area: ES|QL +type: feature +issues: [] From a75301c6e6ac473bfd3fdb3d7945d88ca8667ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 31 Oct 2025 14:43:41 +0100 Subject: [PATCH 24/54] Fix benchmark --- .../benchmark/compute/operator/EvalBenchmark.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/EvalBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/EvalBenchmark.java index a7dff00b46e3f..4c0995bae53c5 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/EvalBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/EvalBenchmark.java @@ -217,7 +217,12 @@ private static EvalOperator.ExpressionEvaluator evaluator(String operation) { ); yield EvalMapper.toEvaluator( FOLD_CONTEXT, - new DateTrunc(Source.EMPTY, new Literal(Source.EMPTY, Duration.ofHours(24), DataType.TIME_DURATION), timestamp), + new DateTrunc( + Source.EMPTY, + new Literal(Source.EMPTY, Duration.ofHours(24), DataType.TIME_DURATION), + timestamp, + configuration() + ), layout(timestamp) ).get(driverContext); } From 7e5c01d025520743ea173c4598094a5e705de87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 31 Oct 2025 17:02:26 +0100 Subject: [PATCH 25/54] Added CSV tests for timezones on affected functions --- .../src/main/resources/bucket.csv-spec | 19 ++++++++++ .../src/main/resources/date_trunc.csv-spec | 38 +++++++++++++++++++ .../src/main/resources/tbucket.csv-spec | 21 ++++++++++ .../xpack/esql/action/EsqlCapabilities.java | 5 +++ 4 files changed, 83 insertions(+) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec index 6d2336c8efb63..4137c09d37a74 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec @@ -988,3 +988,22 @@ COUNT(*):long | bucket:datetime 1 | 1986-07-09T00:00:00.000Z 1 | 1986-09-17T00:00:00.000Z ; + +bucketWithTimezone +required_capability: date_trunc_timezone_support + +SET time_zone = "+05:00"\; +FROM employees +| WHERE hire_date >= "1980-01-01T00:00:00Z" +| STATS by hire_date, bucketHours = BUCKET(hire_date, 3 hours), bucketDay = BUCKET(hire_date, 1 day) +| SORT hire_date +| LIMIT 5 +; + +hire_date:date | bucketHours:date | bucketDay:date +1985-02-18T00:00:00.000Z | 1985-02-17T22:00:00.000Z | 1985-02-17T19:00:00.000Z +1985-02-24T00:00:00.000Z | 1985-02-23T22:00:00.000Z | 1985-02-23T19:00:00.000Z +1985-05-13T00:00:00.000Z | 1985-05-12T22:00:00.000Z | 1985-05-12T19:00:00.000Z +1985-07-09T00:00:00.000Z | 1985-07-08T22:00:00.000Z | 1985-07-08T19:00:00.000Z +1985-09-17T00:00:00.000Z | 1985-09-16T22:00:00.000Z | 1985-09-16T19:00:00.000Z +; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_trunc.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_trunc.csv-spec index a65285b8ab340..c64ff4e783beb 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_trunc.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_trunc.csv-spec @@ -545,3 +545,41 @@ ROW a = 1 a:integer | year_hired:date 1 | 1991-01-01T00:00:00.000Z ; + +dateTruncTimezonesTimeDuration +required_capability: date_trunc_timezone_support + +SET time_zone = "+10:00"\; +ROW date=["2025-03-04T10:09:08.07Z"]::date +| MV_EXPAND date +| EVAL + _1second = DATE_TRUNC(1 second, date), + _1minute = DATE_TRUNC(1 minute, date), + _1hour = DATE_TRUNC(1 hour, date), + _3hours = DATE_TRUNC(3 hours, date), + _24hours = DATE_TRUNC(24 hours, date) +| DROP date +; + +_1second:date | _1minute:date | _1hour:date | _3hours:date | _24hours:date +2025-03-04T10:09:08Z | 2025-03-04T10:09:00Z | 2025-03-04T10:00:00Z | 2025-03-04T08:00:00Z | 2025-03-03T14:00:00Z +; + +dateTruncTimezonesDatePeriod +required_capability: date_trunc_timezone_support + +SET time_zone = "+10:00"\; +ROW date=["2025-03-04T17:09:08.07Z"]::date +| MV_EXPAND date +| EVAL + _1day = DATE_TRUNC(1 day, date), + _7days = DATE_TRUNC(7 days, date), + _1month = DATE_TRUNC(1 month, date), + _3months = DATE_TRUNC(3 months, date), + _1year = DATE_TRUNC(1 year, date) +| DROP date +; + +_1day:date | _7days:date | _1month:date | _3months:date | _1year:date +2025-03-04T14:00:00Z | 2025-03-02T14:00:00Z | 2025-02-28T14:00:00Z | 2024-12-31T14:00:00Z | 2024-12-31T14:00:00Z +; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/tbucket.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/tbucket.csv-spec index ee8cfc86a3b1a..d3b2f964a8a86 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/tbucket.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/tbucket.csv-spec @@ -346,3 +346,24 @@ day_total:long | hour_med:double | h:datetime | message:keyword 1 | 1756467.0 | 2023-10-23T13:00:00.000Z | Connected to 10.1.0.1 1 | 1232382.0 | 2023-10-23T13:00:00.000Z | Disconnected ; + + +tbucketWithTimezone +required_capability: date_trunc_timezone_support + +SET time_zone = "+05:00"\; +FROM employees +| RENAME hire_date as @timestamp +| WHERE @timestamp >= "1980-01-01T00:00:00Z" +| STATS by @timestamp, bucketHours = TBUCKET(3 hours), bucketDay = TBUCKET(1 day) +| SORT @timestamp +| LIMIT 5 +; + +@timestamp:date | bucketHours:date | bucketDay:date +1985-02-18T00:00:00.000Z | 1985-02-17T22:00:00.000Z | 1985-02-17T19:00:00.000Z +1985-02-24T00:00:00.000Z | 1985-02-23T22:00:00.000Z | 1985-02-23T19:00:00.000Z +1985-05-13T00:00:00.000Z | 1985-05-12T22:00:00.000Z | 1985-05-12T19:00:00.000Z +1985-07-09T00:00:00.000Z | 1985-07-08T22:00:00.000Z | 1985-07-08T19:00:00.000Z +1985-09-17T00:00:00.000Z | 1985-09-16T22:00:00.000Z | 1985-09-16T19:00:00.000Z +; 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 698aaac21218f..d4103d4278895 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 @@ -1289,6 +1289,11 @@ public enum Cap { */ SET_COMMAND, + /** + * Support timezones in DATE_TRUNC and dependent functions. + */ + DATE_TRUNC_TIMEZONE_SUPPORT(Build.current().isSnapshot()), + /** * (Re)Added EXPLAIN command */ From 544c4cf2e016785aaae8961f8f69ac4e1737b27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 31 Oct 2025 17:19:12 +0100 Subject: [PATCH 26/54] Fixed Rounding builders being reused --- .../xpack/esql/ConfigurationTestUtils.java | 1 - .../expression/function/grouping/Bucket.java | 50 ++++++++++--------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java index 36445291eeee9..ce59002fda037 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java @@ -40,7 +40,6 @@ import static org.elasticsearch.test.ESTestCase.randomRealisticUnicodeOfLength; import static org.elasticsearch.test.ESTestCase.randomZone; import static org.elasticsearch.xpack.esql.EsqlTestUtils.randomLiteral; -import static org.elasticsearch.xpack.esql.EsqlTestUtils.tables; import static org.elasticsearch.xpack.esql.session.Configuration.QUERY_COMPRESS_THRESHOLD_CHARS; /** diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java index 9d66392a08f43..6378734ea39cb 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java @@ -43,6 +43,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Function; import static org.elasticsearch.common.logging.LoggerMessageFormat.format; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; @@ -69,26 +70,29 @@ public class Bucket extends GroupingFunction.EvaluatableGroupingFunction // TODO maybe we should just cover the whole of representable dates here - like ten years, 100 years, 1000 years, all the way up. // That way you never end up with more than the target number of buckets. - private static final Rounding.Builder LARGEST_HUMAN_DATE_ROUNDING = Rounding.builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY); - private static final Rounding.Builder[] HUMAN_DATE_ROUNDINGS = new Rounding.Builder[] { - Rounding.builder(Rounding.DateTimeUnit.MONTH_OF_YEAR), - Rounding.builder(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR), - Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH), - Rounding.builder(TimeValue.timeValueHours(12)), - Rounding.builder(TimeValue.timeValueHours(3)), - Rounding.builder(TimeValue.timeValueHours(1)), - Rounding.builder(TimeValue.timeValueMinutes(30)), - Rounding.builder(TimeValue.timeValueMinutes(10)), - Rounding.builder(TimeValue.timeValueMinutes(5)), - Rounding.builder(TimeValue.timeValueMinutes(1)), - Rounding.builder(TimeValue.timeValueSeconds(30)), - Rounding.builder(TimeValue.timeValueSeconds(10)), - Rounding.builder(TimeValue.timeValueSeconds(5)), - Rounding.builder(TimeValue.timeValueSeconds(1)), - Rounding.builder(TimeValue.timeValueMillis(100)), - Rounding.builder(TimeValue.timeValueMillis(50)), - Rounding.builder(TimeValue.timeValueMillis(10)), - Rounding.builder(TimeValue.timeValueMillis(1)), }; + private static final Function LARGEST_HUMAN_DATE_ROUNDING = (zoneId) -> Rounding.builder( + Rounding.DateTimeUnit.YEAR_OF_CENTURY + ).timeZone(zoneId).build(); + private static final List> HUMAN_DATE_ROUNDINGS = List.of( + (zoneId) -> Rounding.builder(Rounding.DateTimeUnit.MONTH_OF_YEAR).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueHours(12)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueHours(3)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueHours(1)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueMinutes(30)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueMinutes(10)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueMinutes(5)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueMinutes(1)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueSeconds(30)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueSeconds(10)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueSeconds(5)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueSeconds(1)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueMillis(100)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueMillis(50)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueMillis(10)).timeZone(zoneId).build(), + (zoneId) -> Rounding.builder(TimeValue.timeValueMillis(1)).timeZone(zoneId).build() + ); private final Configuration configuration; private final Expression field; @@ -331,9 +335,9 @@ public Rounding.Prepared getDateRounding(FoldContext foldContext, Long min, Long private record DateRoundingPicker(int buckets, long from, long to, ZoneId zoneId) { Rounding pickRounding() { - Rounding prev = LARGEST_HUMAN_DATE_ROUNDING.timeZone(zoneId).build(); - for (Rounding.Builder builder : HUMAN_DATE_ROUNDINGS) { - Rounding r = builder.timeZone(zoneId).build(); + Rounding prev = LARGEST_HUMAN_DATE_ROUNDING.apply(zoneId); + for (Function roundingMaker : HUMAN_DATE_ROUNDINGS) { + Rounding r = roundingMaker.apply(zoneId); if (roundingIsOk(r)) { prev = r; } else { From e499df461ac6f7a9b29c515b5a77117793ae551c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 31 Oct 2025 17:56:50 +0100 Subject: [PATCH 27/54] Use DateTrunc zoneId instead of configuration in rule --- .../esql/expression/function/grouping/Bucket.java | 4 ---- .../expression/function/scalar/date/DateTrunc.java | 6 +++++- .../local/ReplaceDateTruncBucketWithRoundTo.java | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java index 6378734ea39cb..1a2261a6f3b21 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java @@ -239,10 +239,6 @@ private Bucket(StreamInput in) throws IOException { ); } - public Configuration configuration() { - return configuration; - } - private static List fields(Expression field, Expression buckets, Expression from, Expression to) { List list = new ArrayList<>(4); list.add(field); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java index 6990aac4f4e20..d6b4add29de91 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java @@ -120,6 +120,10 @@ public Expression field() { return timestampField; } + public ZoneId zoneId() { + return configuration().zoneId(); + } + @Override protected TypeResolution resolveType() { if (childrenResolved() == false) { @@ -252,7 +256,7 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { "Function [" + sourceText() + "] has invalid interval [" + interval.sourceText() + "]. " + e.getMessage() ); } - return evaluator(dataType(), source(), fieldEvaluator, createRounding(foldedInterval, configuration().zoneId(), null, null)); + return evaluator(dataType(), source(), fieldEvaluator, createRounding(foldedInterval, zoneId(), null, null)); } public static ExpressionEvaluator.Factory evaluator( diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundTo.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundTo.java index f6db69ab8b142..3fa7c1b57a9a7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundTo.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundTo.java @@ -53,34 +53,34 @@ public class ReplaceDateTruncBucketWithRoundTo extends ParameterizedRule substitute(eval, context)) : plan; + return context.searchStats() != null ? plan.transformUp(Eval.class, eval -> substitute(eval, context.searchStats())) : plan; } - private LogicalPlan substitute(Eval eval, LocalLogicalOptimizerContext context) { + private LogicalPlan substitute(Eval eval, SearchStats searchStats) { // check the filter in children plans - return eval.transformExpressionsOnly(Function.class, f -> substitute(f, eval, context)); + return eval.transformExpressionsOnly(Function.class, f -> substitute(f, eval, searchStats)); } /** * Perform the actual substitution with {@code SearchStats} and predicates in the query. */ - private Expression substitute(Expression e, Eval eval, LocalLogicalOptimizerContext context) { + private Expression substitute(Expression e, Eval eval, SearchStats searchStats) { Expression roundTo = null; if (e instanceof DateTrunc dateTrunc) { roundTo = maybeSubstituteWithRoundTo( dateTrunc.source(), dateTrunc.field(), dateTrunc.interval(), - context.searchStats(), + searchStats, eval, - (interval, minValue, maxValue) -> DateTrunc.createRounding(interval, context.configuration().zoneId(), minValue, maxValue) + (interval, minValue, maxValue) -> DateTrunc.createRounding(interval, dateTrunc.zoneId(), minValue, maxValue) ); } else if (e instanceof Bucket bucket) { roundTo = maybeSubstituteWithRoundTo( bucket.source(), bucket.field(), bucket.buckets(), - context.searchStats(), + searchStats, eval, (interval, minValue, maxValue) -> bucket.getDateRounding(FoldContext.small(), minValue, maxValue) ); From 205a15c16493d3b69e574eb0fbe3a2b3a66a4ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 31 Oct 2025 18:07:13 +0100 Subject: [PATCH 28/54] Undo roundings test --- .../scalar/date/DateTruncRoundingTests.java | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java index 6bedcd7334dc4..eab34bcfd98e4 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java @@ -13,8 +13,8 @@ import java.time.Duration; import java.time.Instant; import java.time.Period; -import java.time.ZoneOffset; +import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc.createRounding; import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc.processDatetime; import static org.hamcrest.Matchers.containsString; @@ -27,73 +27,73 @@ public class DateTruncRoundingTests extends ESTestCase { public void testCreateRoundingDuration() { Rounding.Prepared rounding; - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Duration.ofHours(0))); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createRounding(Duration.ofHours(0))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Duration.ofHours(-10))); + e = expectThrows(IllegalArgumentException.class, () -> createRounding(Duration.ofHours(-10))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - rounding = createUtcRounding(Duration.ofHours(1)); + rounding = createRounding(Duration.ofHours(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.HOUR_OF_DAY), 0d); - rounding = createUtcRounding(Duration.ofHours(10)); + rounding = createRounding(Duration.ofHours(10)); assertEquals(10, rounding.roundingSize(Rounding.DateTimeUnit.HOUR_OF_DAY), 0d); - rounding = createUtcRounding(Duration.ofMinutes(1)); + rounding = createRounding(Duration.ofMinutes(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d); - rounding = createUtcRounding(Duration.ofMinutes(100)); + rounding = createRounding(Duration.ofMinutes(100)); assertEquals(100, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d); - rounding = createUtcRounding(Duration.ofSeconds(1)); + rounding = createRounding(Duration.ofSeconds(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), 0d); - rounding = createUtcRounding(Duration.ofSeconds(120)); + rounding = createRounding(Duration.ofSeconds(120)); assertEquals(120, rounding.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), 0d); - rounding = createUtcRounding(Duration.ofSeconds(60).plusMinutes(5).plusHours(1)); + rounding = createRounding(Duration.ofSeconds(60).plusMinutes(5).plusHours(1)); assertEquals(1 + 5 + 60, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d); } public void testCreateRoundingPeriod() { Rounding.Prepared rounding; - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Period.ofMonths(0))); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createRounding(Period.ofMonths(0))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Period.ofYears(-10))); + e = expectThrows(IllegalArgumentException.class, () -> createRounding(Period.ofYears(-10))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Period.of(0, 1, 1))); + e = expectThrows(IllegalArgumentException.class, () -> createRounding(Period.of(0, 1, 1))); assertThat(e.getMessage(), containsString("Time interval with multiple periods is not supported")); - rounding = createUtcRounding(Period.ofDays(1)); + rounding = createRounding(Period.ofDays(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.DAY_OF_MONTH), 0d); - rounding = createUtcRounding(Period.ofDays(4)); + rounding = createRounding(Period.ofDays(4)); assertEquals(4, rounding.roundingSize(Rounding.DateTimeUnit.DAY_OF_MONTH), 0d); - rounding = createUtcRounding(Period.ofDays(7)); + rounding = createRounding(Period.ofDays(7)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR), 0d); - rounding = createUtcRounding(Period.ofMonths(1)); + rounding = createRounding(Period.ofMonths(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MONTH_OF_YEAR), 0d); - rounding = createUtcRounding(Period.ofMonths(3)); + rounding = createRounding(Period.ofMonths(3)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.QUARTER_OF_YEAR), 0d); - rounding = createUtcRounding(Period.ofMonths(5)); + rounding = createRounding(Period.ofMonths(5)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MONTH_OF_YEAR), 0d); - rounding = createUtcRounding(Period.ofYears(1)); + rounding = createRounding(Period.ofYears(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.YEAR_OF_CENTURY), 0d); - rounding = createUtcRounding(Period.ofYears(3)); + rounding = createRounding(Period.ofYears(3)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.YEAR_OF_CENTURY), 0d); } public void testCreateRoundingNullInterval() { - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding((Period) null)); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createRounding(null)); assertThat(e.getMessage(), containsString("Time interval is not supported")); } @@ -102,11 +102,11 @@ public void testDateTruncFunction() { IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> processDatetime(ts, createUtcRounding(Period.ofDays(-1))) + () -> processDatetime(ts, createRounding(Period.ofDays(-1))) ); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> processDatetime(ts, createUtcRounding(Duration.ofHours(-1)))); + e = expectThrows(IllegalArgumentException.class, () -> processDatetime(ts, createRounding(Duration.ofHours(-1)))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); } @@ -114,12 +114,4 @@ private static long toMillis(String timestamp) { return Instant.parse(timestamp).toEpochMilli(); } - private static Rounding.Prepared createUtcRounding(Duration duration) { - return DateTrunc.createRounding(duration, ZoneOffset.UTC, null, null); - } - - private static Rounding.Prepared createUtcRounding(Period period) { - return DateTrunc.createRounding(period, ZoneOffset.UTC, null, null); - } - } From 46debec4787dd5ca7d561b42a1cb20ee1ef32f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 31 Oct 2025 18:25:24 +0100 Subject: [PATCH 29/54] Fix tests --- .../expression/function/grouping/Bucket.java | 4 ++ .../scalar/date/DateTruncRoundingTests.java | 56 +++++++++++-------- .../function/scalar/date/DateTruncTests.java | 7 +-- ...eplaceDateTruncBucketWithRoundToTests.java | 19 +++---- 4 files changed, 45 insertions(+), 41 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java index 1a2261a6f3b21..6378734ea39cb 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java @@ -239,6 +239,10 @@ private Bucket(StreamInput in) throws IOException { ); } + public Configuration configuration() { + return configuration; + } + private static List fields(Expression field, Expression buckets, Expression from, Expression to) { List list = new ArrayList<>(4); list.add(field); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java index eab34bcfd98e4..6bedcd7334dc4 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncRoundingTests.java @@ -13,8 +13,8 @@ import java.time.Duration; import java.time.Instant; import java.time.Period; +import java.time.ZoneOffset; -import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc.createRounding; import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc.processDatetime; import static org.hamcrest.Matchers.containsString; @@ -27,73 +27,73 @@ public class DateTruncRoundingTests extends ESTestCase { public void testCreateRoundingDuration() { Rounding.Prepared rounding; - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createRounding(Duration.ofHours(0))); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Duration.ofHours(0))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> createRounding(Duration.ofHours(-10))); + e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Duration.ofHours(-10))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - rounding = createRounding(Duration.ofHours(1)); + rounding = createUtcRounding(Duration.ofHours(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.HOUR_OF_DAY), 0d); - rounding = createRounding(Duration.ofHours(10)); + rounding = createUtcRounding(Duration.ofHours(10)); assertEquals(10, rounding.roundingSize(Rounding.DateTimeUnit.HOUR_OF_DAY), 0d); - rounding = createRounding(Duration.ofMinutes(1)); + rounding = createUtcRounding(Duration.ofMinutes(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d); - rounding = createRounding(Duration.ofMinutes(100)); + rounding = createUtcRounding(Duration.ofMinutes(100)); assertEquals(100, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d); - rounding = createRounding(Duration.ofSeconds(1)); + rounding = createUtcRounding(Duration.ofSeconds(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), 0d); - rounding = createRounding(Duration.ofSeconds(120)); + rounding = createUtcRounding(Duration.ofSeconds(120)); assertEquals(120, rounding.roundingSize(Rounding.DateTimeUnit.SECOND_OF_MINUTE), 0d); - rounding = createRounding(Duration.ofSeconds(60).plusMinutes(5).plusHours(1)); + rounding = createUtcRounding(Duration.ofSeconds(60).plusMinutes(5).plusHours(1)); assertEquals(1 + 5 + 60, rounding.roundingSize(Rounding.DateTimeUnit.MINUTE_OF_HOUR), 0d); } public void testCreateRoundingPeriod() { Rounding.Prepared rounding; - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createRounding(Period.ofMonths(0))); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Period.ofMonths(0))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> createRounding(Period.ofYears(-10))); + e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Period.ofYears(-10))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> createRounding(Period.of(0, 1, 1))); + e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding(Period.of(0, 1, 1))); assertThat(e.getMessage(), containsString("Time interval with multiple periods is not supported")); - rounding = createRounding(Period.ofDays(1)); + rounding = createUtcRounding(Period.ofDays(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.DAY_OF_MONTH), 0d); - rounding = createRounding(Period.ofDays(4)); + rounding = createUtcRounding(Period.ofDays(4)); assertEquals(4, rounding.roundingSize(Rounding.DateTimeUnit.DAY_OF_MONTH), 0d); - rounding = createRounding(Period.ofDays(7)); + rounding = createUtcRounding(Period.ofDays(7)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR), 0d); - rounding = createRounding(Period.ofMonths(1)); + rounding = createUtcRounding(Period.ofMonths(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MONTH_OF_YEAR), 0d); - rounding = createRounding(Period.ofMonths(3)); + rounding = createUtcRounding(Period.ofMonths(3)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.QUARTER_OF_YEAR), 0d); - rounding = createRounding(Period.ofMonths(5)); + rounding = createUtcRounding(Period.ofMonths(5)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.MONTH_OF_YEAR), 0d); - rounding = createRounding(Period.ofYears(1)); + rounding = createUtcRounding(Period.ofYears(1)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.YEAR_OF_CENTURY), 0d); - rounding = createRounding(Period.ofYears(3)); + rounding = createUtcRounding(Period.ofYears(3)); assertEquals(1, rounding.roundingSize(Rounding.DateTimeUnit.YEAR_OF_CENTURY), 0d); } public void testCreateRoundingNullInterval() { - IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createRounding(null)); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createUtcRounding((Period) null)); assertThat(e.getMessage(), containsString("Time interval is not supported")); } @@ -102,11 +102,11 @@ public void testDateTruncFunction() { IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> processDatetime(ts, createRounding(Period.ofDays(-1))) + () -> processDatetime(ts, createUtcRounding(Period.ofDays(-1))) ); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); - e = expectThrows(IllegalArgumentException.class, () -> processDatetime(ts, createRounding(Duration.ofHours(-1)))); + e = expectThrows(IllegalArgumentException.class, () -> processDatetime(ts, createUtcRounding(Duration.ofHours(-1)))); assertThat(e.getMessage(), containsString("Zero or negative time interval is not supported")); } @@ -114,4 +114,12 @@ private static long toMillis(String timestamp) { return Instant.parse(timestamp).toEpochMilli(); } + private static Rounding.Prepared createUtcRounding(Duration duration) { + return DateTrunc.createRounding(duration, ZoneOffset.UTC, null, null); + } + + private static Rounding.Prepared createUtcRounding(Period period) { + return DateTrunc.createRounding(period, ZoneOffset.UTC, null, null); + } + } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index b5a39e75f0bce..0ad259d6b9da0 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -95,9 +95,7 @@ public static List makeTruncDurationTestCases() { new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "UTC", "2020-01-01T03:00:00Z"), new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z"), new DurationTestCaseData(Duration.ofMinutes(3 * 60), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z"), - - // TODO: What to do with hours/minutes/seconds that are not divisors of a full day? - // new DurationTestCaseData(Duration.ofHours(5), "2020-01-01T05:30:00Z", "+01", "2020-01-01T05:00:00Z"), + new DurationTestCaseData(Duration.ofHours(5), "2020-01-01T05:30:00Z", "+01", "2020-01-01T05:00:00Z"), /// /// Daylight savings @@ -116,8 +114,7 @@ public static List makeTruncDurationTestCases() { // Bigger intervals new DurationTestCaseData(Duration.ofHours(12), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), new DurationTestCaseData(Duration.ofHours(24), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), - // TODO: What to do with hours over a day? - // new DurationTestCaseData(Duration.ofHours(48), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00") + new DurationTestCaseData(Duration.ofHours(48), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), /// /// Partial hours timezones diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundToTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundToTests.java index eae62ceea55cc..93805d0754663 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundToTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/local/ReplaceDateTruncBucketWithRoundToTests.java @@ -34,7 +34,6 @@ import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.esql.EsqlTestUtils.as; import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME; -import static org.hamcrest.CoreMatchers.equalTo; //@TestLogging(value = "org.elasticsearch.xpack.esql:TRACE", reason = "debug") public class ReplaceDateTruncBucketWithRoundToTests extends LocalLogicalPlanOptimizerTests { @@ -102,7 +101,7 @@ public void testSubstituteDateTruncInEvalWithRoundTo() { assertEquals(1, fields.size()); Alias a = fields.get(0); assertEquals("x", a.name()); - verifySubstitution(a, roundToPointsSize, configuration); + verifySubstitution(a, roundToPointsSize); LogicalPlan subPlan = predicateString.isEmpty() ? eval : eval.child(); EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } @@ -126,7 +125,7 @@ public void testSubstituteDateTruncInAggWithRoundTo() { assertEquals(1, fields.size()); Alias a = fields.get(0); assertEquals("x", a.name()); - verifySubstitution(a, roundToPointsSize, configuration); + verifySubstitution(a, roundToPointsSize); LogicalPlan subPlan = predicateString.isEmpty() ? eval : eval.child(); EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } @@ -150,7 +149,7 @@ public void testSubstituteBucketInAggWithRoundTo() { assertEquals(1, fields.size()); Alias a = fields.get(0); assertEquals("x", a.name()); - verifySubstitution(a, roundToPointsSize, configuration); + verifySubstitution(a, roundToPointsSize); LogicalPlan subPlan = predicateString.isEmpty() ? eval : eval.child(); EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } @@ -181,7 +180,7 @@ public void testSubstituteDateTruncInEvalWithRoundToWithEvalRename() { assertEquals(dateTruncOnExpression ? 2 : 1, fields.size()); Alias a = fields.get(dateTruncOnExpression ? 1 : 0); assertEquals("y", a.name()); - verifySubstitution(a, roundToPointsSize, configuration); + verifySubstitution(a, roundToPointsSize); LogicalPlan subPlan = hasWhere ? eval.child() : eval; EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } @@ -209,7 +208,7 @@ public void testSubstituteBucketInAggWithRoundToWithEvalRename() { assertEquals(dateTruncOnExpression ? 2 : 1, fields.size()); Alias a = fields.get(dateTruncOnExpression ? 1 : 0); assertEquals("y", a.name()); - verifySubstitution(a, roundToPointsSize, configuration); + verifySubstitution(a, roundToPointsSize); LogicalPlan subPlan = hasWhere ? eval.child() : eval; EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } @@ -237,13 +236,13 @@ public void testSubstituteDateTruncInAggWithRoundToWithEvalRename() { assertEquals(dateTruncOnExpression ? 2 : 1, fields.size()); Alias a = fields.get(dateTruncOnExpression ? 1 : 0); assertEquals("y", a.name()); - verifySubstitution(a, roundToPointsSize, configuration); + verifySubstitution(a, roundToPointsSize); LogicalPlan subPlan = hasWhere ? eval.child() : eval; EsRelation relation = as(subPlan.children().get(0), EsRelation.class); } } - private void verifySubstitution(Alias a, int roundToPointsSize, Configuration configuration) { + private void verifySubstitution(Alias a, int roundToPointsSize) { FieldAttribute fa = null; Expression e = a.child(); if (roundToPointsSize > 0) { @@ -252,20 +251,16 @@ private void verifySubstitution(Alias a, int roundToPointsSize, Configuration co assertEquals(roundToPointsSize, roundTo.points().size()); } else if (roundToPointsSize == 0) { if (e instanceof DateTrunc dateTrunc) { - assertThat(dateTrunc.configuration(), equalTo(configuration)); fa = as(dateTrunc.field(), FieldAttribute.class); } else if (e instanceof Bucket bucket) { - assertThat(bucket.configuration(), equalTo(configuration)); fa = as(bucket.field(), FieldAttribute.class); } else { fail(e.getClass() + " is not supported"); } } else { if (e instanceof DateTrunc dateTrunc) { - assertThat(dateTrunc.configuration(), equalTo(configuration)); assertTrue(dateTrunc.field() instanceof ReferenceAttribute); } else if (e instanceof Bucket bucket) { - assertThat(bucket.configuration(), equalTo(configuration)); assertTrue(bucket.field() instanceof ReferenceAttribute); } else { fail(e.getClass() + " is not supported"); From 29707e580c6db25fad6b3efcea0c137b127a9828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Mon, 3 Nov 2025 14:14:04 +0100 Subject: [PATCH 30/54] Fixed tests and their testcase names --- .../function/grouping/BucketTests.java | 8 ++-- .../function/grouping/TBucketTests.java | 8 ++-- .../function/scalar/date/DateTruncTests.java | 40 ++++++++++++++----- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java index f2062eeb83239..81c1d970a7a2b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java @@ -145,7 +145,7 @@ private static void dateTruncCases(List suppliers) { .map( data -> List.of( new TestCaseSupplier( - "period, millis; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForMillis(), List.of(DataType.DATETIME, DataType.DATE_PERIOD), () -> new TestCaseSupplier.TestCase( List.of( @@ -158,7 +158,7 @@ private static void dateTruncCases(List suppliers) { ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( - "period, nanos; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForNanos(), List.of(DataType.DATE_NANOS, DataType.DATE_PERIOD), () -> new TestCaseSupplier.TestCase( List.of( @@ -182,7 +182,7 @@ private static void dateTruncCases(List suppliers) { .map( data -> List.of( new TestCaseSupplier( - "duration, millis; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForMillis(), List.of(DataType.DATETIME, DataType.TIME_DURATION), () -> new TestCaseSupplier.TestCase( List.of( @@ -195,7 +195,7 @@ private static void dateTruncCases(List suppliers) { ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( - "duration, nanos; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForNanos(), List.of(DataType.DATE_NANOS, DataType.TIME_DURATION), () -> new TestCaseSupplier.TestCase( List.of( diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java index 043c91009edfe..3961bd9997829 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java @@ -130,7 +130,7 @@ private static void dateTruncCases(List suppliers) { .map( data -> List.of( new TestCaseSupplier( - "period, millis; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForMillis(), List.of(DataType.DATE_PERIOD, DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of( @@ -143,7 +143,7 @@ private static void dateTruncCases(List suppliers) { ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( - "period, nanos; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForNanos(), List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( List.of( @@ -167,7 +167,7 @@ private static void dateTruncCases(List suppliers) { .map( data -> List.of( new TestCaseSupplier( - "duration, millis; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForMillis(), List.of(DataType.TIME_DURATION, DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of( @@ -180,7 +180,7 @@ private static void dateTruncCases(List suppliers) { ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( - "duration, nanos; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForNanos(), List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( List.of( diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 0ad259d6b9da0..74fdcb4e003ae 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -55,6 +55,16 @@ public static Iterable parameters() { } public record PeriodTestCaseData(Period period, String inputDate, @Nullable String zoneIdString, String expectedDate) { + public String testCaseNameForMillis() { + var zoneIdName = zoneIdString == null ? "random" : zoneIdString; + return "period, millis; " + period + ", " + zoneIdName + ", " + inputDate; + } + + public String testCaseNameForNanos() { + var zoneIdName = zoneIdString == null ? "random" : zoneIdString; + return "period, nanos; " + period + ", " + zoneIdName + ", " + inputDate; + } + public ZoneId zoneId() { return zoneIdString == null ? randomZone() : ZoneId.of(zoneIdString); } @@ -65,6 +75,16 @@ public long inputDateAsMillis() { } public record DurationTestCaseData(Duration duration, String inputDate, @Nullable String zoneIdString, String expectedDate) { + public String testCaseNameForMillis() { + var zoneIdName = zoneIdString == null ? "random" : zoneIdString; + return "duration, millis; " + duration + ", " + zoneIdName + ", " + inputDate; + } + + public String testCaseNameForNanos() { + var zoneIdName = zoneIdString == null ? "random" : zoneIdString; + return "duration, nanos; " + duration + ", " + zoneIdName + ", " + inputDate; + } + public ZoneId zoneId() { return zoneIdString == null ? randomZone() : ZoneId.of(zoneIdString); } @@ -78,24 +98,24 @@ public static List makeTruncDurationTestCases() { String ts = "2023-02-17T10:25:33.38Z"; return List.of( /// - /// Timezone agnostic (<2h intervals) + /// Timezone agnostic (<=1m intervals) /// new DurationTestCaseData(Duration.ofMillis(100), ts, null, "2023-02-17T10:25:33.30Z"), new DurationTestCaseData(Duration.ofSeconds(1), ts, null, "2023-02-17T10:25:33Z"), new DurationTestCaseData(Duration.ofMinutes(1), ts, null, "2023-02-17T10:25:00Z"), - new DurationTestCaseData(Duration.ofHours(1), ts, null, "2023-02-17T10:00:00Z"), new DurationTestCaseData(Duration.ofSeconds(30), ts, null, "2023-02-17T10:25:30Z"), - new DurationTestCaseData(Duration.ofMinutes(15), ts, null, "2023-02-17T10:15:00Z"), /// - /// Timezone dependent (>=2h intervals) + /// Timezone dependent (>1m intervals) /// + new DurationTestCaseData(Duration.ofHours(1), ts, "UTC", "2023-02-17T10:00:00Z"), + new DurationTestCaseData(Duration.ofMinutes(15), ts, "UTC", "2023-02-17T10:15:00Z"), new DurationTestCaseData(Duration.ofHours(3), ts, "UTC", "2023-02-17T09:00:00Z"), new DurationTestCaseData(Duration.ofHours(3), ts, "-02:00", "2023-02-17T08:00:00Z"), new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "UTC", "2020-01-01T03:00:00Z"), new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z"), new DurationTestCaseData(Duration.ofMinutes(3 * 60), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z"), - new DurationTestCaseData(Duration.ofHours(5), "2020-01-01T05:30:00Z", "+01", "2020-01-01T05:00:00Z"), + new DurationTestCaseData(Duration.ofHours(5), "2020-01-01T05:30:00Z", "+01", "2020-01-01T01:00:00Z"), /// /// Daylight savings @@ -114,7 +134,7 @@ public static List makeTruncDurationTestCases() { // Bigger intervals new DurationTestCaseData(Duration.ofHours(12), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), new DurationTestCaseData(Duration.ofHours(24), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), - new DurationTestCaseData(Duration.ofHours(48), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(48), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-25T00:00:00+02:00"), /// /// Partial hours timezones @@ -172,7 +192,7 @@ public static List makeTruncPeriodTestCases() { private static List ofDatePeriod(PeriodTestCaseData data) { return List.of( new TestCaseSupplier( - "period, millis; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForMillis(), List.of(DataType.DATE_PERIOD, DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of( @@ -185,7 +205,7 @@ private static List ofDatePeriod(PeriodTestCaseData data) { ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( - "period, nanos; " + data.period() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForNanos(), List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( List.of( @@ -203,7 +223,7 @@ private static List ofDatePeriod(PeriodTestCaseData data) { private static List ofDuration(DurationTestCaseData data) { return List.of( new TestCaseSupplier( - "period, millis; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForMillis(), List.of(DataType.TIME_DURATION, DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of( @@ -216,7 +236,7 @@ private static List ofDuration(DurationTestCaseData data) { ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( - "period, nanos; " + data.duration() + ", " + data.zoneIdString() + ", " + data.inputDate(), + data.testCaseNameForNanos(), List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( List.of( From e3ae6bac9c7b684b0352dc352e4eed8976af7304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Tue, 4 Nov 2025 17:49:22 +0100 Subject: [PATCH 31/54] Added tests for locale and timezone --- .../xpack/esql/ConfigurationTestUtils.java | 4 ++ .../matchers/StringBytesRefMatcher.java | 44 ++++++++++++ .../expression/function/TestCaseSupplier.java | 15 +--- .../function/grouping/BucketTests.java | 13 ++-- .../function/grouping/TBucketTests.java | 5 +- ...AbstractConfigurationFunctionTestCase.java | 13 +++- .../scalar/date/DateExtractTests.java | 69 ++++++++++++------- .../function/scalar/date/DateFormatTests.java | 51 +++++++++++++- .../function/scalar/date/DayNameTests.java | 47 ++++++++----- .../function/scalar/date/MonthNameTests.java | 55 +++++++++------ .../function/scalar/date/NowTests.java | 25 +++---- .../function/scalar/string/ToLowerTests.java | 39 +++-------- .../function/scalar/string/ToUpperTests.java | 39 +++-------- 13 files changed, 258 insertions(+), 161 deletions(-) create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java index ce59002fda037..af784827be003 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/ConfigurationTestUtils.java @@ -80,6 +80,10 @@ public static Configuration randomConfiguration(String query, Map { + private final String string; + private final BytesRef bytesRef; + + public StringBytesRefMatcher(String string) { + this.string = string; + this.bytesRef = new BytesRef(string); + } + + @Override + public boolean matches(Object item) { + return item instanceof BytesRef && item.equals(bytesRef); + } + + @Override + public void describeMismatch(Object item, org.hamcrest.Description description) { + description.appendText("was "); + if (item instanceof BytesRef br) { + description.appendValue(br.utf8ToString()); + } else { + description.appendValue(item); + } + } + + @Override + public void describeTo(org.hamcrest.Description description) { + description.appendText(string); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java index 04c6811619ca0..984ec58d76cea 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java @@ -1579,7 +1579,7 @@ public static List mapTestCases( ) { return suppliers.stream() .map(supplier -> new TestCaseSupplier(supplier.name(), supplier.types(), () -> mapper.apply(supplier.get()))) - .toList(); + .collect(Collectors.toCollection(ArrayList::new)); } public static final class TestCase { @@ -1795,19 +1795,6 @@ public Object extra() { return extra; } - /** - * Build a new {@link TestCase} with the {@link #TEST_CONFIGURATION}. - *

- * The source is also set to match the configuration - *

- * - * @deprecated Use a custom configuration instead, and test the results. - */ - @Deprecated - public TestCase withStaticConfiguration() { - return withConfiguration(TEST_SOURCE, TEST_CONFIGURATION); - } - /** * Build a new {@link TestCase} with new {@link #configuration}. *

diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java index 81c1d970a7a2b..bae88cfac0951 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java @@ -30,6 +30,7 @@ import java.time.Duration; import java.time.Instant; import java.time.Period; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; import java.util.function.LongSupplier; @@ -120,7 +121,7 @@ private static void dateCases(List suppliers, String name, Lon + "rounding=Rounding[DAY_OF_MONTH in Z][fixed to midnight]]", DataType.DATETIME, resultsMatcher(args) - ).withStaticConfiguration(); + ).withConfiguration(TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)); })); // same as above, but a low bucket count and datetime bounds that match it (at hour span) suppliers.add(new TestCaseSupplier(name, List.of(DataType.DATETIME, DataType.INTEGER, fromType, toType), () -> { @@ -134,7 +135,7 @@ private static void dateCases(List suppliers, String name, Lon "DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding[3600000 in Z][fixed]]", DataType.DATETIME, equalTo(Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY).build().prepareForUnknown().round(date.getAsLong())) - ).withStaticConfiguration(); + ).withConfiguration(TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)); })); } } @@ -243,7 +244,7 @@ private static void dateCasesWithSpan( "DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding" + spanStr + "]", DataType.DATETIME, resultsMatcher(args) - ).withStaticConfiguration(); + ).withConfiguration(TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)); })); } @@ -264,7 +265,7 @@ private static void dateNanosCasesWithSpan( Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, resultsMatcher(args) - ).withStaticConfiguration(); + ).withConfiguration(TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)); })); } @@ -283,7 +284,7 @@ private static void dateNanosCases(List suppliers, String name Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, resultsMatcher(args) - ).withStaticConfiguration(); + ).withConfiguration(TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)); })); // same as above, but a low bucket count and datetime bounds that match it (at hour span) suppliers.add(new TestCaseSupplier(name, List.of(DataType.DATE_NANOS, DataType.INTEGER, fromType, toType), () -> { @@ -297,7 +298,7 @@ private static void dateNanosCases(List suppliers, String name Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, equalTo(Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY).build().prepareForUnknown().round(date.getAsLong())) - ).withStaticConfiguration(); + ).withConfiguration(TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)); })); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java index 3961bd9997829..7dc4a0104c635 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java @@ -29,6 +29,7 @@ import java.time.Duration; import java.time.Instant; import java.time.Period; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; import java.util.function.LongSupplier; @@ -101,7 +102,7 @@ private static void dateCasesWithSpan( "DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding" + spanStr + "]", DataType.DATETIME, resultsMatcher(args) - ).withStaticConfiguration(); + ).withConfiguration(TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)); })); } @@ -121,7 +122,7 @@ private static void dateNanosCasesWithSpan( Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, resultsMatcher(args) - ).withStaticConfiguration(); + ).withConfiguration(TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)); })); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java index 5a677c076f5dc..8d6e26034202c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/AbstractConfigurationFunctionTestCase.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.esql.expression.function.scalar; -import org.elasticsearch.xpack.esql.ConfigurationBuilder; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; @@ -15,8 +14,10 @@ import java.time.ZoneId; import java.util.List; +import java.util.Locale; import static org.elasticsearch.xpack.esql.ConfigurationTestUtils.randomConfiguration; +import static org.elasticsearch.xpack.esql.ConfigurationTestUtils.randomConfigurationBuilder; import static org.elasticsearch.xpack.esql.ConfigurationTestUtils.randomTables; import static org.elasticsearch.xpack.esql.SerializationTestUtils.assertSerialization; import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; @@ -48,6 +49,14 @@ public void testSerializationWithConfiguration() { } protected static Configuration configurationForTimezone(ZoneId zoneId) { - return new ConfigurationBuilder(randomConfiguration()).query(TEST_SOURCE.text()).zoneId(zoneId).build(); + return randomConfigurationBuilder().query(TEST_SOURCE.text()).zoneId(zoneId).build(); + } + + protected static Configuration configurationForLocale(Locale locale) { + return randomConfigurationBuilder().query(TEST_SOURCE.text()).locale(locale).build(); + } + + protected static Configuration configurationForTimezoneAndLocale(ZoneId zoneId, Locale locale) { + return randomConfigurationBuilder().query(TEST_SOURCE.text()).zoneId(zoneId).locale(locale).build(); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java index fb7ec0ddb3897..26e427a76fc95 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateExtractTests.java @@ -11,6 +11,7 @@ import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.compute.operator.DriverContext; import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.core.InvalidArgumentException; @@ -24,12 +25,15 @@ import org.elasticsearch.xpack.esql.session.Configuration; import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.ChronoField; import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; +import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; @@ -46,30 +50,6 @@ public static Iterable parameters() { for (var stringType : DataType.stringTypes()) { suppliers.addAll( List.of( - new TestCaseSupplier( - List.of(stringType, DataType.DATETIME), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(new BytesRef("YeAr"), stringType, "chrono"), - new TestCaseSupplier.TypedData(1687944333000L, DataType.DATETIME, "date") - ), - "DateExtractMillisEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", - DataType.LONG, - equalTo(2023L) - ).withStaticConfiguration() - ), - new TestCaseSupplier( - List.of(stringType, DataType.DATE_NANOS), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(new BytesRef("YeAr"), stringType, "chrono"), - new TestCaseSupplier.TypedData(1687944333000000000L, DataType.DATE_NANOS, "date") - ), - "DateExtractNanosEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", - DataType.LONG, - equalTo(2023L) - ).withStaticConfiguration() - ), new TestCaseSupplier( List.of(stringType, DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( @@ -80,7 +60,7 @@ public static Iterable parameters() { "DateExtractNanosEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", DataType.LONG, equalTo(123456L) - ).withStaticConfiguration() + ).withConfiguration(TestCaseSupplier.TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)) ), new TestCaseSupplier( List.of(stringType, DataType.DATETIME), @@ -93,7 +73,7 @@ public static Iterable parameters() { "DateExtractMillisEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=Z]", DataType.LONG, is(nullValue()) - ).withStaticConfiguration() + ).withConfiguration(TestCaseSupplier.TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)) .withWarning( "Line 1:1: evaluation of [source] failed, treating result as null. Only first 20 failures recorded." ) @@ -107,9 +87,46 @@ public static Iterable parameters() { ); } + suppliers.addAll(casesFor("YeAr", "2023-11-04T16:13:12Z", "Z", 2023)); + suppliers.addAll(casesFor("day_of_month", "2020-01-01T00:00:00Z", "America/New_York", 31)); + suppliers.addAll(casesFor("month_of_year", "2020-06-30T23:00:00Z", "Europe/Paris", 7)); + return parameterSuppliersFromTypedDataWithDefaultChecks(true, suppliers); } + private static List casesFor(String field, String date, String zoneIdName, long expectedResult) { + long dateMillis = Instant.parse(date).toEpochMilli(); + ZoneId zoneId = ZoneId.of(zoneIdName); + return List.of( + new TestCaseSupplier( + field + " - " + date + " (millis) - " + zoneId, + List.of(DataType.KEYWORD, DataType.DATETIME), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(field, DataType.KEYWORD, "field"), + new TestCaseSupplier.TypedData(dateMillis, DataType.DATETIME, "date") + ), + "DateExtractMillisEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=" + zoneId + "]", + DataType.LONG, + equalTo(expectedResult) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) + ), + new TestCaseSupplier( + field + " - " + date + " (nanos) - " + zoneId, + List.of(DataType.KEYWORD, DataType.DATE_NANOS), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(field, DataType.KEYWORD, "field"), + new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(dateMillis), DataType.DATE_NANOS, "date") + ), + "DateExtractNanosEvaluator[value=Attribute[channel=1], chronoField=Attribute[channel=0], zone=" + zoneId + "]", + DataType.LONG, + equalTo(expectedResult) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) + ) + ); + } + public void testAllChronoFields() { long epochMilli = 1687944333123L; ZonedDateTime date = Instant.ofEpochMilli(epochMilli).atZone(EsqlTestUtils.TEST_CFG.zoneId()); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java index 10ead11f09273..0b631c70f33a5 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java @@ -13,6 +13,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateUtils; +import org.elasticsearch.xpack.esql.common.matchers.StringBytesRefMatcher; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -24,8 +25,10 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.function.Supplier; +import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; import static org.hamcrest.Matchers.matchesPattern; public class DateFormatTests extends AbstractConfigurationFunctionTestCase { @@ -84,10 +87,56 @@ public static Iterable parameters() { (value) -> new BytesRef(EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER.formatNanos(DateUtils.toLong((Instant) value))), List.of() ); - suppliers = TestCaseSupplier.mapTestCases(suppliers, testCase -> testCase.withStaticConfiguration()); + suppliers = TestCaseSupplier.mapTestCases( + suppliers, + testCase -> testCase.withConfiguration(TestCaseSupplier.TEST_SOURCE, configurationForLocale(Locale.US)) + ); + suppliers.addAll(casesFor("MMM", "2020-01-01T00:00:00.00Z", "es-es", "ene")); + suppliers.addAll(casesFor("VV", "2020-01-01T00:00:00.00Z", "es-es", "Z")); return parameterSuppliersFromTypedDataWithDefaultChecks(true, suppliers); } + private static List casesFor(String format, String date, String localeTag, String expectedString) { + long dateMillis = Instant.parse(date).toEpochMilli(); + Locale locale = Locale.forLanguageTag(localeTag); + return List.of( + new TestCaseSupplier( + format + " - " + date + " (millis) - " + locale, + List.of(DataType.KEYWORD, DataType.DATETIME), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(format, DataType.KEYWORD, "format"), + new TestCaseSupplier.TypedData(dateMillis, DataType.DATETIME, "date") + ), + matchesPattern( + "DateFormatMillisEvaluator\\[val=Attribute\\[channel=1], formatter=Attribute\\[(channel=0|\\w+)], locale=" + + locale + + "]" + ), + DataType.KEYWORD, + new StringBytesRefMatcher(expectedString) + ).withConfiguration(TEST_SOURCE, configurationForLocale(locale)) + ), + new TestCaseSupplier( + format + " - " + date + " (nanos) - " + locale, + List.of(DataType.KEYWORD, DataType.DATE_NANOS), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(format, DataType.KEYWORD, "format"), + new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(dateMillis), DataType.DATE_NANOS, "date") + ), + matchesPattern( + "DateFormatNanosEvaluator\\[val=Attribute\\[channel=1], formatter=Attribute\\[(channel=0|\\w+)], locale=" + + locale + + "]" + ), + DataType.KEYWORD, + new StringBytesRefMatcher(expectedString) + ).withConfiguration(TEST_SOURCE, configurationForLocale(locale)) + ) + ); + } + @Override protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { return new DateFormat(source, args.get(0), args.size() == 2 ? args.get(1) : null, 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 index b50377660c8c5..c36ca1f3e0a59 100644 --- 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 @@ -10,11 +10,11 @@ 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.analysis.AnalyzerSettings; +import org.elasticsearch.xpack.esql.common.matchers.StringBytesRefMatcher; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.Literal; @@ -46,48 +46,61 @@ public DayNameTests(@Name("TestCase") Supplier testCa @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")); + + // UTC, English + suppliers.addAll(generateTest("2019-03-11T00:00:00.00Z", "Monday", "Z", "en")); + suppliers.addAll(generateTest("2022-07-26T23:59:59.99Z", "Tuesday", "Z", "en")); + suppliers.addAll(generateTest("2017-10-11T23:12:32.12Z", "Wednesday", "Z", "en")); + suppliers.addAll(generateTest("2023-01-05T07:39:01.28Z", "Thursday", "Z", "en")); + suppliers.addAll(generateTest("2023-02-17T10:25:33.38Z", "Friday", "Z", "en")); + suppliers.addAll(generateTest("2013-06-15T22:55:33.82Z", "Saturday", "Z", "en")); + suppliers.addAll(generateTest("2024-08-18T01:01:29.49Z", "Sunday", "Z", "en")); + + // Other timezones and locales + suppliers.addAll(generateTest("2019-03-11T22:00:00.00Z", "Tuesday", "+05:00", "en")); + suppliers.addAll(generateTest("2019-03-11T00:00:00.00Z", "Sunday", "America/New_York", "en")); + suppliers.addAll(generateTest("2019-03-11T00:00:00.00Z", "lunes", "Z", "es")); + suppliers.addAll(generateTest("2019-03-11T00:00:00.00Z", "domingo", "America/New_York", "es")); suppliers.add( new TestCaseSupplier( + "Null", 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]"), + Matchers.startsWith("DayNameMillisEvaluator[val=Attribute[channel=0], zoneId="), DataType.KEYWORD, equalTo(null) - ).withStaticConfiguration() + ) ) ); return parameterSuppliersFromTypedDataWithDefaultChecks(true, suppliers); } - private static List generateTest(String dateTime, String expectedWeekDay) { + private static List generateTest(String dateTime, String expectedWeekDay, String zoneIdString, String localeTag) { + ZoneId zoneId = ZoneId.of(zoneIdString); + Locale locale = Locale.forLanguageTag(localeTag); return List.of( new TestCaseSupplier( + dateTime + " (millis) - " + zoneId + ", " + locale, 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]"), + Matchers.startsWith("DayNameMillisEvaluator[val=Attribute[channel=0], zoneId=" + zoneId + ", locale=" + locale + "]"), DataType.KEYWORD, - equalTo(new BytesRef(expectedWeekDay)) - ).withStaticConfiguration() + new StringBytesRefMatcher(expectedWeekDay) + ).withConfiguration(TestCaseSupplier.TEST_SOURCE, configurationForTimezoneAndLocale(zoneId, locale)) ), new TestCaseSupplier( + dateTime + " (nanos) - " + zoneId + ", " + locale, 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]"), + Matchers.is("DayNameNanosEvaluator[val=Attribute[channel=0], zoneId=" + zoneId + ", locale=" + locale + "]"), DataType.KEYWORD, - equalTo(new BytesRef(expectedWeekDay)) - ).withStaticConfiguration() + new StringBytesRefMatcher(expectedWeekDay) + ).withConfiguration(TestCaseSupplier.TEST_SOURCE, configurationForTimezoneAndLocale(zoneId, locale)) ) ); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java index 9c4e6c2007b12..d308e5663ccb2 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java @@ -10,11 +10,11 @@ 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.analysis.AnalyzerSettings; +import org.elasticsearch.xpack.esql.common.matchers.StringBytesRefMatcher; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.Literal; @@ -51,53 +51,64 @@ protected Expression buildWithConfiguration(Source source, List args @ParametersFactory public static Iterable parameters() { List suppliers = new ArrayList<>(); - suppliers.addAll(generateTest("1994-01-19T00:00:00.00Z", "January")); - suppliers.addAll(generateTest("1995-02-20T23:59:59.99Z", "February")); - suppliers.addAll(generateTest("1996-03-21T23:12:32.12Z", "March")); - suppliers.addAll(generateTest("1997-04-22T07:39:01.28Z", "April")); - suppliers.addAll(generateTest("1998-05-23T10:25:33.38Z", "May")); - suppliers.addAll(generateTest("1999-06-24T22:55:33.82Z", "June")); - suppliers.addAll(generateTest("2000-07-25T01:01:29.49Z", "July")); - suppliers.addAll(generateTest("2001-08-25T01:01:29.49Z", "August")); - suppliers.addAll(generateTest("2002-09-25T01:01:29.49Z", "September")); - suppliers.addAll(generateTest("2003-10-25T01:01:29.49Z", "October")); - suppliers.addAll(generateTest("2004-11-25T01:01:29.49Z", "November")); - suppliers.addAll(generateTest("2005-12-25T01:01:29.49Z", "December")); + suppliers.addAll(generateTest("1994-01-19T00:00:00.00Z", "January", "Z", "en")); + suppliers.addAll(generateTest("1995-02-20T23:59:59.99Z", "February", "Z", "en")); + suppliers.addAll(generateTest("1996-03-21T23:12:32.12Z", "March", "Z", "en")); + suppliers.addAll(generateTest("1997-04-22T07:39:01.28Z", "April", "Z", "en")); + suppliers.addAll(generateTest("1998-05-23T10:25:33.38Z", "May", "Z", "en")); + suppliers.addAll(generateTest("1999-06-24T22:55:33.82Z", "June", "Z", "en")); + suppliers.addAll(generateTest("2000-07-25T01:01:29.49Z", "July", "Z", "en")); + suppliers.addAll(generateTest("2001-08-25T01:01:29.49Z", "August", "Z", "en")); + suppliers.addAll(generateTest("2002-09-25T01:01:29.49Z", "September", "Z", "en")); + suppliers.addAll(generateTest("2003-10-25T01:01:29.49Z", "October", "Z", "en")); + suppliers.addAll(generateTest("2004-11-25T01:01:29.49Z", "November", "Z", "en")); + suppliers.addAll(generateTest("2005-12-25T01:01:29.49Z", "December", "Z", "en")); + + // Other timezones and locales + suppliers.addAll(generateTest("2019-03-31T22:00:00.00Z", "April", "+05:00", "en")); + suppliers.addAll(generateTest("2019-03-01T00:00:00.00Z", "February", "America/New_York", "en")); + suppliers.addAll(generateTest("2019-03-11T00:00:00.00Z", "marzo", "Z", "es")); + suppliers.addAll(generateTest("2019-03-01T00:00:00.00Z", "febrero", "America/New_York", "es")); suppliers.add( new TestCaseSupplier( + "Null", List.of(DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of(new TestCaseSupplier.TypedData(null, DataType.DATETIME, "date")), - Matchers.startsWith("MonthNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), + Matchers.startsWith("MonthNameMillisEvaluator[val=Attribute[channel=0], zoneId="), DataType.KEYWORD, equalTo(null) - ).withStaticConfiguration() + ) ) ); return parameterSuppliersFromTypedDataWithDefaultChecks(true, suppliers); } - private static List generateTest(String dateTime, String expectedMonthName) { + private static List generateTest(String dateTime, String expectedMonthName, String zoneIdString, String localeTag) { + ZoneId zoneId = ZoneId.of(zoneIdString); + Locale locale = Locale.forLanguageTag(localeTag); return List.of( new TestCaseSupplier( + expectedMonthName, List.of(DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of(new TestCaseSupplier.TypedData(toMillis(dateTime), DataType.DATETIME, "date")), - Matchers.startsWith("MonthNameMillisEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), + Matchers.startsWith("MonthNameMillisEvaluator[val=Attribute[channel=0], zoneId=" + zoneId + ", locale=" + locale + "]"), DataType.KEYWORD, - equalTo(new BytesRef(expectedMonthName)) - ).withStaticConfiguration() + new StringBytesRefMatcher(expectedMonthName) + ).withConfiguration(TestCaseSupplier.TEST_SOURCE, configurationForTimezoneAndLocale(zoneId, locale)) ), new TestCaseSupplier( + expectedMonthName, List.of(DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( List.of(new TestCaseSupplier.TypedData(toNanos(dateTime), DataType.DATE_NANOS, "date")), - Matchers.is("MonthNameNanosEvaluator[val=Attribute[channel=0], zoneId=Z, locale=en_US]"), + Matchers.is("MonthNameNanosEvaluator[val=Attribute[channel=0], zoneId=" + zoneId + ", locale=" + locale + "]"), DataType.KEYWORD, - equalTo(new BytesRef(expectedMonthName)) - ).withStaticConfiguration() + new StringBytesRefMatcher(expectedMonthName) + ).withConfiguration(TestCaseSupplier.TEST_SOURCE, configurationForTimezoneAndLocale(zoneId, locale)) ) ); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java index ef5d6c2a18950..2c3ed6e8cdf88 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/NowTests.java @@ -10,6 +10,7 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import org.elasticsearch.xpack.esql.ConfigurationTestUtils; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -31,21 +32,15 @@ public NowTests(@Name("TestCase") Supplier testCaseSu @ParametersFactory public static Iterable parameters() { - return parameterSuppliersFromTypedDataWithDefaultChecks( - true, - List.of( - new TestCaseSupplier( - "Now Test", - List.of(), - () -> new TestCaseSupplier.TestCase( - List.of(), - matchesPattern("LiteralsEvaluator\\[lit=.*]"), - DataType.DATETIME, - equalTo(TestCaseSupplier.TEST_CONFIGURATION.now().toInstant().toEpochMilli()) - ).withStaticConfiguration() - ) - ) - ); + return parameterSuppliersFromTypedDataWithDefaultChecks(true, List.of(new TestCaseSupplier("Now Test", List.of(), () -> { + var configuration = ConfigurationTestUtils.randomConfigurationBuilder().query(TestCaseSupplier.TEST_SOURCE.text()).build(); + return new TestCaseSupplier.TestCase( + List.of(), + matchesPattern("LiteralsEvaluator\\[lit=.*]"), + DataType.DATETIME, + equalTo(configuration.now().toInstant().toEpochMilli()) + ).withConfiguration(TestCaseSupplier.TEST_SOURCE, configuration); + }))); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java index 894b1249426ec..ea05b7f75b5bc 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToLowerTests.java @@ -12,25 +12,22 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.lucene.BytesRefs; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.compute.data.Block; import org.elasticsearch.compute.data.Page; +import org.elasticsearch.xpack.esql.ConfigurationTestUtils; import org.elasticsearch.xpack.esql.EsqlTestUtils; -import org.elasticsearch.xpack.esql.analysis.AnalyzerSettings; 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.core.util.DateUtils; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; -import org.elasticsearch.xpack.esql.plugin.QueryPragmas; import org.elasticsearch.xpack.esql.session.Configuration; import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.Locale; import java.util.function.Supplier; import static org.hamcrest.Matchers.equalTo; @@ -53,30 +50,11 @@ public static Iterable parameters() { public void testRandomLocale() { String testString = randomAlphaOfLength(10); - Configuration cfg = randomLocaleConfig(); + Configuration cfg = ConfigurationTestUtils.randomConfiguration(); ToLower func = new ToLower(Source.EMPTY, Literal.keyword(Source.EMPTY, testString), cfg); assertThat(BytesRefs.toBytesRef(testString.toLowerCase(cfg.locale())), equalTo(func.fold(FoldContext.small()))); } - private Configuration randomLocaleConfig() { - return new Configuration( - DateUtils.UTC, - randomLocale(random()), - null, - null, - QueryPragmas.EMPTY, - AnalyzerSettings.QUERY_RESULT_TRUNCATION_MAX_SIZE.getDefault(Settings.EMPTY), - AnalyzerSettings.QUERY_RESULT_TRUNCATION_DEFAULT_SIZE.getDefault(Settings.EMPTY), - "", - false, - Map.of(), - System.nanoTime(), - randomBoolean(), - AnalyzerSettings.QUERY_TIMESERIES_RESULT_TRUNCATION_MAX_SIZE.getDefault(Settings.EMPTY), - AnalyzerSettings.QUERY_TIMESERIES_RESULT_TRUNCATION_DEFAULT_SIZE.getDefault(Settings.EMPTY) - ); - } - @Override protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { return new ToLower(source, args.get(0), configuration); @@ -91,8 +69,10 @@ private static void suppliers(List suppliers, String name, Dat values.add(new TestCaseSupplier.TypedData(new BytesRef(value), type, "0")); String expectedValue = value.toLowerCase(EsqlTestUtils.TEST_CFG.locale()); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))) - .withStaticConfiguration(); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))).withConfiguration( + TestCaseSupplier.TEST_SOURCE, + configurationForLocale(Locale.US) + ); })); suppliers.add(new TestCaseSupplier(name + " mv", List.of(type), () -> { List values = new ArrayList<>(); @@ -102,7 +82,10 @@ private static void suppliers(List suppliers, String name, Dat values.add(new TestCaseSupplier.TypedData(strings.stream().map(BytesRef::new).toList(), type, "0")); List expectedValue = strings.stream().map(s -> new BytesRef(s.toLowerCase(EsqlTestUtils.TEST_CFG.locale()))).toList(); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withStaticConfiguration(); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withConfiguration( + TestCaseSupplier.TEST_SOURCE, + configurationForLocale(Locale.US) + ); })); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java index c3ab1ae0c2614..a4d8d5083dfbd 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/ToUpperTests.java @@ -12,25 +12,22 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.lucene.BytesRefs; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.compute.data.Block; import org.elasticsearch.compute.data.Page; +import org.elasticsearch.xpack.esql.ConfigurationTestUtils; import org.elasticsearch.xpack.esql.EsqlTestUtils; -import org.elasticsearch.xpack.esql.analysis.AnalyzerSettings; 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.core.util.DateUtils; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; import org.elasticsearch.xpack.esql.expression.function.scalar.AbstractConfigurationFunctionTestCase; -import org.elasticsearch.xpack.esql.plugin.QueryPragmas; import org.elasticsearch.xpack.esql.session.Configuration; import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.Locale; import java.util.function.Supplier; import static org.hamcrest.Matchers.equalTo; @@ -53,30 +50,11 @@ public static Iterable parameters() { public void testRandomLocale() { String testString = randomAlphaOfLength(10); - Configuration cfg = randomLocaleConfig(); + Configuration cfg = ConfigurationTestUtils.randomConfiguration(); ToUpper func = new ToUpper(Source.EMPTY, Literal.keyword(Source.EMPTY, testString), cfg); assertThat(BytesRefs.toBytesRef(testString.toUpperCase(cfg.locale())), equalTo(func.fold(FoldContext.small()))); } - private Configuration randomLocaleConfig() { - return new Configuration( - DateUtils.UTC, - randomLocale(random()), - null, - null, - QueryPragmas.EMPTY, - AnalyzerSettings.QUERY_RESULT_TRUNCATION_MAX_SIZE.getDefault(Settings.EMPTY), - AnalyzerSettings.QUERY_RESULT_TRUNCATION_DEFAULT_SIZE.getDefault(Settings.EMPTY), - "", - false, - Map.of(), - System.nanoTime(), - randomBoolean(), - AnalyzerSettings.QUERY_TIMESERIES_RESULT_TRUNCATION_MAX_SIZE.getDefault(Settings.EMPTY), - AnalyzerSettings.QUERY_TIMESERIES_RESULT_TRUNCATION_DEFAULT_SIZE.getDefault(Settings.EMPTY) - ); - } - @Override protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { return new ToUpper(source, args.get(0), configuration); @@ -91,8 +69,10 @@ private static void supplier(List suppliers, String name, Data values.add(new TestCaseSupplier.TypedData(new BytesRef(value), type, "0")); String expectedValue = value.toUpperCase(EsqlTestUtils.TEST_CFG.locale()); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))) - .withStaticConfiguration(); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(new BytesRef(expectedValue))).withConfiguration( + TestCaseSupplier.TEST_SOURCE, + configurationForLocale(Locale.US) + ); })); suppliers.add(new TestCaseSupplier(name + " mv", List.of(type), () -> { List values = new ArrayList<>(); @@ -102,7 +82,10 @@ private static void supplier(List suppliers, String name, Data values.add(new TestCaseSupplier.TypedData(strings.stream().map(BytesRef::new).toList(), type, "0")); List expectedValue = strings.stream().map(s -> new BytesRef(s.toUpperCase(EsqlTestUtils.TEST_CFG.locale()))).toList(); - return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withStaticConfiguration(); + return new TestCaseSupplier.TestCase(values, expectedToString, type, equalTo(expectedValue)).withConfiguration( + TestCaseSupplier.TEST_SOURCE, + configurationForLocale(Locale.US) + ); })); } From 096541fff4daedda223d48d89d90f46397c25908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 5 Nov 2025 12:52:42 +0100 Subject: [PATCH 32/54] Use TypeSafeMatcher on BytesRef matcher --- .../common/matchers/StringBytesRefMatcher.java | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java index 4c0143e9d4053..95d6600e20135 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java @@ -8,12 +8,12 @@ package org.elasticsearch.xpack.esql.common.matchers; import org.apache.lucene.util.BytesRef; -import org.hamcrest.BaseMatcher; +import org.hamcrest.TypeSafeMatcher; /** * Test matcher for ESQL BytesRef that expects BytesRefs, but describes the errors as strings, for better readability. */ -public class StringBytesRefMatcher extends BaseMatcher { +public class StringBytesRefMatcher extends TypeSafeMatcher { private final String string; private final BytesRef bytesRef; @@ -23,18 +23,13 @@ public StringBytesRefMatcher(String string) { } @Override - public boolean matches(Object item) { - return item instanceof BytesRef && item.equals(bytesRef); + protected boolean matchesSafely(BytesRef item) { + return item.equals(bytesRef); } @Override - public void describeMismatch(Object item, org.hamcrest.Description description) { - description.appendText("was "); - if (item instanceof BytesRef br) { - description.appendValue(br.utf8ToString()); - } else { - description.appendValue(item); - } + public void describeMismatchSafely(BytesRef item, org.hamcrest.Description description) { + description.appendText("was ").appendValue(item.utf8ToString()); } @Override From b9100bd190ee0b02b70df7a885acda37d61b0bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 5 Nov 2025 12:53:11 +0100 Subject: [PATCH 33/54] Removed ESQL dependency from the matcher docs --- .../xpack/esql/common/matchers/StringBytesRefMatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java index 95d6600e20135..94d484bb17c18 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java @@ -11,7 +11,7 @@ import org.hamcrest.TypeSafeMatcher; /** - * Test matcher for ESQL BytesRef that expects BytesRefs, but describes the errors as strings, for better readability. + * Test matcher for BytesRef that expects BytesRefs, but describes the errors as strings, for better readability. */ public class StringBytesRefMatcher extends TypeSafeMatcher { private final String string; From de3dd41197f4b73e959bbda3ef0ee5496ba3b43d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 5 Nov 2025 13:22:07 +0100 Subject: [PATCH 34/54] Moved matchers to server.test --- .../elasticsearch/test/ReadableMatchers.java | 86 +++++++++++++++++++ .../common/matchers/DateMillisMatcher.java | 48 ----------- .../common/matchers/DateNanosMatcher.java | 49 ----------- .../function/grouping/BucketTests.java | 12 +-- .../function/grouping/TBucketTests.java | 12 +-- .../function/scalar/date/DateTruncTests.java | 12 +-- 6 files changed, 104 insertions(+), 115 deletions(-) create mode 100644 test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java delete mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateMillisMatcher.java delete mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateNanosMatcher.java diff --git a/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java b/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java new file mode 100644 index 0000000000000..53c2f3e7d62bc --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.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 + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.test; + +import org.elasticsearch.common.time.DateFormatter; +import org.elasticsearch.common.time.DateUtils; +import org.hamcrest.TypeSafeMatcher; + +import java.time.Instant; + +public class ReadableMatchers { + private static final DateFormatter dateFormatter = DateFormatter.forPattern("strict_date_optional_time"); + + /** + * Test matcher for millis dates that expects longs, but describes the errors as dates, for better readability. + *

+ * See {@link #matchesDateNanos} for the nanos counterpart. + *

+ */ + public static DateMillisMatcher matchesDateMillis(String date) { + return new DateMillisMatcher(date); + } + + /** + * Test matcher for nanos dates that expects longs, but describes the errors as dates, for better readability. + *

+ * See {@link DateMillisMatcher} for the millis counterpart. + *

+ */ + public static DateNanosMatcher matchesDateNanos(String date) { + return new DateNanosMatcher(date); + } + + public static class DateMillisMatcher extends TypeSafeMatcher { + private final long timeMillis; + + public DateMillisMatcher(String date) { + this.timeMillis = Instant.parse(date).toEpochMilli(); + } + + @Override + public boolean matchesSafely(Long item) { + return timeMillis == item; + } + + @Override + public void describeMismatchSafely(Long item, org.hamcrest.Description description) { + description.appendText("was ").appendValue(dateFormatter.formatMillis(item)); + } + + @Override + public void describeTo(org.hamcrest.Description description) { + description.appendText(dateFormatter.formatMillis(timeMillis)); + } + } + + public static class DateNanosMatcher extends TypeSafeMatcher { + private final long timeNanos; + + public DateNanosMatcher(String date) { + this.timeNanos = DateUtils.toLong(Instant.parse(date)); + } + + @Override + public boolean matchesSafely(Long item) { + return timeNanos == item; + } + + @Override + public void describeMismatchSafely(Long item, org.hamcrest.Description description) { + description.appendText("was ").appendValue(dateFormatter.formatNanos(item)); + } + + @Override + public void describeTo(org.hamcrest.Description description) { + description.appendText(dateFormatter.formatNanos(timeNanos)); + } + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateMillisMatcher.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateMillisMatcher.java deleted file mode 100644 index 74d80fd3806ec..0000000000000 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateMillisMatcher.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.common.matchers; - -import org.hamcrest.BaseMatcher; - -import java.time.Instant; - -import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER; - -/** - * Test matcher for ESQL datetimes that expects longs, but describes the errors as dates, for better readability. - *

- * See {@link DateNanosMatcher} for the date nanos counterpart. - *

- */ -public class DateMillisMatcher extends BaseMatcher { - private final long timeMillis; - - public DateMillisMatcher(String date) { - this.timeMillis = Instant.parse(date).toEpochMilli(); - } - - @Override - public boolean matches(Object item) { - return item instanceof Long && timeMillis == (Long) item; - } - - @Override - public void describeMismatch(Object item, org.hamcrest.Description description) { - description.appendText("was "); - if (item instanceof Long l) { - description.appendValue(DEFAULT_DATE_TIME_FORMATTER.formatMillis(l)); - } else { - description.appendValue(item); - } - } - - @Override - public void describeTo(org.hamcrest.Description description) { - description.appendText(DEFAULT_DATE_TIME_FORMATTER.formatMillis(timeMillis)); - } -} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateNanosMatcher.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateNanosMatcher.java deleted file mode 100644 index 38856bd31c1a1..0000000000000 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/DateNanosMatcher.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.common.matchers; - -import org.elasticsearch.common.time.DateUtils; -import org.hamcrest.BaseMatcher; - -import java.time.Instant; - -import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER; - -/** - * Test matcher for ESQL date nanos that expects longs, but describes the errors as dates, for better readability. - *

- * See {@link DateMillisMatcher} for the datetime (millis) counterpart. - *

- */ -public class DateNanosMatcher extends BaseMatcher { - private final long timeNanos; - - public DateNanosMatcher(String date) { - this.timeNanos = DateUtils.toLong(Instant.parse(date)); - } - - @Override - public boolean matches(Object item) { - return item instanceof Long && timeNanos == (Long) item; - } - - @Override - public void describeMismatch(Object item, org.hamcrest.Description description) { - description.appendText("was "); - if (item instanceof Long l) { - description.appendValue(DEFAULT_DATE_TIME_FORMATTER.formatNanos(l)); - } else { - description.appendValue(item); - } - } - - @Override - public void describeTo(org.hamcrest.Description description) { - description.appendText(DEFAULT_DATE_TIME_FORMATTER.formatNanos(timeNanos)); - } -} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java index 81c1d970a7a2b..0d71b067651c1 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java @@ -15,8 +15,6 @@ import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.logging.LogManager; -import org.elasticsearch.xpack.esql.common.matchers.DateMillisMatcher; -import org.elasticsearch.xpack.esql.common.matchers.DateNanosMatcher; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -35,6 +33,8 @@ import java.util.function.LongSupplier; import java.util.function.Supplier; +import static org.elasticsearch.test.ReadableMatchers.matchesDateMillis; +import static org.elasticsearch.test.ReadableMatchers.matchesDateNanos; import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTruncTests.makeTruncDurationTestCases; import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTruncTests.makeTruncPeriodTestCases; @@ -154,7 +154,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - new DateMillisMatcher(data.expectedDate()) + matchesDateMillis(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -171,7 +171,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - new DateNanosMatcher(data.expectedDate()) + matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ) @@ -191,7 +191,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - new DateMillisMatcher(data.expectedDate()) + matchesDateMillis(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -208,7 +208,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - new DateNanosMatcher(data.expectedDate()) + matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java index 3961bd9997829..e52afe5c0c2fa 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java @@ -14,8 +14,6 @@ import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.logging.LogManager; -import org.elasticsearch.xpack.esql.common.matchers.DateMillisMatcher; -import org.elasticsearch.xpack.esql.common.matchers.DateNanosMatcher; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -34,6 +32,8 @@ import java.util.function.LongSupplier; import java.util.function.Supplier; +import static org.elasticsearch.test.ReadableMatchers.matchesDateMillis; +import static org.elasticsearch.test.ReadableMatchers.matchesDateNanos; import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTruncTests.makeTruncDurationTestCases; import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTruncTests.makeTruncPeriodTestCases; @@ -139,7 +139,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - new DateMillisMatcher(data.expectedDate()) + matchesDateMillis(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -156,7 +156,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - new DateNanosMatcher(data.expectedDate()) + matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ) @@ -176,7 +176,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - new DateMillisMatcher(data.expectedDate()) + matchesDateMillis(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -193,7 +193,7 @@ private static void dateTruncCases(List suppliers) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - new DateNanosMatcher(data.expectedDate()) + matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 74fdcb4e003ae..91e9b5ef34ee1 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -12,8 +12,6 @@ import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.core.Nullable; -import org.elasticsearch.xpack.esql.common.matchers.DateMillisMatcher; -import org.elasticsearch.xpack.esql.common.matchers.DateNanosMatcher; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -30,6 +28,8 @@ import java.util.List; import java.util.function.Supplier; +import static org.elasticsearch.test.ReadableMatchers.matchesDateMillis; +import static org.elasticsearch.test.ReadableMatchers.matchesDateNanos; import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; import static org.hamcrest.Matchers.equalTo; @@ -201,7 +201,7 @@ private static List ofDatePeriod(PeriodTestCaseData data) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - new DateMillisMatcher(data.expectedDate()) + matchesDateMillis(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -214,7 +214,7 @@ private static List ofDatePeriod(PeriodTestCaseData data) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - new DateNanosMatcher(data.expectedDate()) + matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); @@ -232,7 +232,7 @@ private static List ofDuration(DurationTestCaseData data) { ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATETIME, - new DateMillisMatcher(data.expectedDate()) + matchesDateMillis(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ), new TestCaseSupplier( @@ -245,7 +245,7 @@ private static List ofDuration(DurationTestCaseData data) { ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, - new DateNanosMatcher(data.expectedDate()) + matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); From ee0af20c47d8d7dc789f7cdf6cb76f6d6be9585a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 5 Nov 2025 14:57:22 +0100 Subject: [PATCH 35/54] Avoid renaming timestamp in TS functions --- .../src/main/resources/tbucket.csv-spec | 36 ++++++++++++------- .../function/scalar/date/DateTruncTests.java | 7 ++++ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/tbucket.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/tbucket.csv-spec index d3b2f964a8a86..131c33ed702b5 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/tbucket.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/tbucket.csv-spec @@ -348,22 +348,34 @@ day_total:long | hour_med:double | h:datetime | message:keyword ; -tbucketWithTimezone +tbucketWithTimezoneAndDuration +required_capability: ts_command_v0 required_capability: date_trunc_timezone_support SET time_zone = "+05:00"\; -FROM employees -| RENAME hire_date as @timestamp -| WHERE @timestamp >= "1980-01-01T00:00:00Z" -| STATS by @timestamp, bucketHours = TBUCKET(3 hours), bucketDay = TBUCKET(1 day) +TS k8s +| WHERE @timestamp == "2024-05-10T00:04:49.000Z" +| STATS by @timestamp, bucket = TBUCKET(3 hours) | SORT @timestamp -| LIMIT 5 +| LIMIT 2 ; -@timestamp:date | bucketHours:date | bucketDay:date -1985-02-18T00:00:00.000Z | 1985-02-17T22:00:00.000Z | 1985-02-17T19:00:00.000Z -1985-02-24T00:00:00.000Z | 1985-02-23T22:00:00.000Z | 1985-02-23T19:00:00.000Z -1985-05-13T00:00:00.000Z | 1985-05-12T22:00:00.000Z | 1985-05-12T19:00:00.000Z -1985-07-09T00:00:00.000Z | 1985-07-08T22:00:00.000Z | 1985-07-08T19:00:00.000Z -1985-09-17T00:00:00.000Z | 1985-09-16T22:00:00.000Z | 1985-09-16T19:00:00.000Z +@timestamp:datetime | bucket:datetime +2024-05-10T00:04:49.000Z | 2024-05-09T22:00:00.000Z +; + +tbucketWithTimezoneAndPeriod +required_capability: ts_command_v0 +required_capability: date_trunc_timezone_support + +SET time_zone = "+05:00"\; +TS k8s +| WHERE @timestamp == "2024-05-10T00:04:49.000Z" +| STATS by @timestamp, bucket = TBUCKET(1 day) +| SORT @timestamp +| LIMIT 2 +; + +@timestamp:datetime | bucket:datetime +2024-05-10T00:04:49.000Z | 2024-05-09T19:00:00.000Z ; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 91e9b5ef34ee1..b0648107eee3e 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -115,6 +115,8 @@ public static List makeTruncDurationTestCases() { new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "UTC", "2020-01-01T03:00:00Z"), new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z"), new DurationTestCaseData(Duration.ofMinutes(3 * 60), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z"), + new DurationTestCaseData(Duration.ofHours(6), "2024-03-01T00:30:00Z", "-03", "2024-02-29T21:00:00Z"), + new DurationTestCaseData(Duration.ofHours(24), "2024-03-01T00:30:00Z", "-03", "2024-02-29T03:00:00Z"), new DurationTestCaseData(Duration.ofHours(5), "2020-01-01T05:30:00Z", "+01", "2020-01-01T01:00:00Z"), /// @@ -165,6 +167,11 @@ public static List makeTruncPeriodTestCases() { new PeriodTestCaseData(Period.ofMonths(7), ts, "UTC", "2022-11-01T00:00:00.00Z"), new PeriodTestCaseData(Period.ofYears(5), ts, "UTC", "2021-01-01T00:00:00.00Z"), + /// + /// Timezones + /// + new PeriodTestCaseData(Period.ofDays(1), "2024-03-01T00:30:00Z", "-03", "2024-02-29T03:00:00Z"), + /// /// Timezone with DST (-5 to -4 at 2025-03-09T02:00:00-05, and -4 to -5 at 2025-11-02T02:00:00-04) /// From 44ad461e802d1ffe5bf7db315ad96513dfd3eb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 5 Nov 2025 16:04:14 +0100 Subject: [PATCH 36/54] Format --- .../java/org/elasticsearch/test/ReadableMatchers.java | 9 +++++---- .../qa/testFixtures/src/main/resources/bucket.csv-spec | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java b/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java index 53c2f3e7d62bc..b90187a8f04c8 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java @@ -11,6 +11,7 @@ import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateUtils; +import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; import java.time.Instant; @@ -51,12 +52,12 @@ public boolean matchesSafely(Long item) { } @Override - public void describeMismatchSafely(Long item, org.hamcrest.Description description) { + public void describeMismatchSafely(Long item, Description description) { description.appendText("was ").appendValue(dateFormatter.formatMillis(item)); } @Override - public void describeTo(org.hamcrest.Description description) { + public void describeTo(Description description) { description.appendText(dateFormatter.formatMillis(timeMillis)); } } @@ -74,12 +75,12 @@ public boolean matchesSafely(Long item) { } @Override - public void describeMismatchSafely(Long item, org.hamcrest.Description description) { + public void describeMismatchSafely(Long item, Description description) { description.appendText("was ").appendValue(dateFormatter.formatNanos(item)); } @Override - public void describeTo(org.hamcrest.Description description) { + public void describeTo(Description description) { description.appendText(dateFormatter.formatNanos(timeNanos)); } } diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec index 4137c09d37a74..d7feccb86ec8c 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/bucket.csv-spec @@ -1000,7 +1000,7 @@ FROM employees | LIMIT 5 ; -hire_date:date | bucketHours:date | bucketDay:date +hire_date:date | bucketHours:date | bucketDay:date 1985-02-18T00:00:00.000Z | 1985-02-17T22:00:00.000Z | 1985-02-17T19:00:00.000Z 1985-02-24T00:00:00.000Z | 1985-02-23T22:00:00.000Z | 1985-02-23T19:00:00.000Z 1985-05-13T00:00:00.000Z | 1985-05-12T22:00:00.000Z | 1985-05-12T19:00:00.000Z From 84b882e487b999fb42e72cb040d968379d5e1b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 5 Nov 2025 18:12:11 +0100 Subject: [PATCH 37/54] Add extra cases to dateCases --- .../expression/function/TestCaseSupplier.java | 33 ++++++++++++++----- .../function/scalar/date/DateTruncTests.java | 4 ++- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java index 04c6811619ca0..e2b7ab4acd921 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java @@ -47,8 +47,10 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.BinaryOperator; +import java.util.function.Consumer; import java.util.function.DoubleFunction; import java.util.function.Function; import java.util.function.IntFunction; @@ -1159,9 +1161,16 @@ public static List dateCases(Instant min, Instant max) { */ public static List dateCases(long min, long max) { List cases = new ArrayList<>(); - if (min <= 0 && max >= 0) { - cases.add(new TypedDataSupplier("<1970-01-01T00:00:00Z>", () -> 0L, DataType.DATETIME)); - } + Consumer addExactCase = (value) -> { + long date = Instant.parse(value).toEpochMilli(); + if (date >= min && date <= max) { + cases.add(new TypedDataSupplier("<" + value + ">", () -> date, DataType.DATETIME)); + } + }; + + addExactCase.accept("1970-01-01T00:00:00Z"); + addExactCase.accept("2025-03-30T01:00:00+01:00"); // Before Europe/Paris DST change + addExactCase.accept("2025-03-30T03:00:00+02:00"); // After Europe/Paris DST change // 1970-01-01T00:00:00Z - 2286-11-20T17:46:40Z long lower1 = Math.max(min, 0); @@ -1186,6 +1195,8 @@ public static List dateCases(long min, long max) { ); } + + return cases; } @@ -1218,11 +1229,17 @@ public static List dateNanosCases(Instant minValue, Instant m Instant twentyTwoFifty = Instant.parse("2250-01-01T00:00:00Z"); List cases = new ArrayList<>(); - if (minValue.isAfter(Instant.EPOCH) == false) { - cases.add( - new TypedDataSupplier("<1970-01-01T00:00:00.000000000Z>", () -> DateUtils.toLong(Instant.EPOCH), DataType.DATE_NANOS) - ); - } + Consumer addExactCase = (value) -> { + Instant instant = Instant.parse(value); + long date = DateUtils.toLong(Instant.parse(value)); + if (minValue.isAfter(instant) == false && maxValue.isBefore(instant) == false) { + cases.add(new TypedDataSupplier("<" + value + ">", () -> date, DataType.DATE_NANOS)); + } + }; + + addExactCase.accept("1970-01-01T00:00:00.000000000Z"); + addExactCase.accept("2025-03-30T01:00:00.000000001+01:00"); // Before Europe/Paris DST change + addExactCase.accept("2025-03-30T03:00:00.000000002+02:00"); // After Europe/Paris DST change Instant lower = Instant.EPOCH.isBefore(minValue) ? minValue : Instant.EPOCH; Instant upper = twentyOneHundred.isAfter(maxValue) ? maxValue : twentyOneHundred; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index b0648107eee3e..933ab3cc5e010 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -173,7 +173,7 @@ public static List makeTruncPeriodTestCases() { new PeriodTestCaseData(Period.ofDays(1), "2024-03-01T00:30:00Z", "-03", "2024-02-29T03:00:00Z"), /// - /// Timezone with DST (-5 to -4 at 2025-03-09T02:00:00-05, and -4 to -5 at 2025-11-02T02:00:00-04) + /// Timezone with DST (e.g. New York: -5 to -4 at 2025-03-09T02:00:00-05, and -4 to -5 at 2025-11-02T02:00:00-04) /// new PeriodTestCaseData(Period.ofDays(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-09T00:00:00-05:00"), new PeriodTestCaseData(Period.ofMonths(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-01T00:00:00-05:00"), @@ -182,6 +182,8 @@ public static List makeTruncPeriodTestCases() { new PeriodTestCaseData(Period.ofDays(1), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-11-02T00:00:00-04:00"), new PeriodTestCaseData(Period.ofDays(7), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-10-27T00:00:00-04:00"), new PeriodTestCaseData(Period.ofMonths(3), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-10-01T00:00:00-04:00"), + new PeriodTestCaseData(Period.ofDays(1), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), + new PeriodTestCaseData(Period.ofMonths(3), "2025-11-02T05:00:00-05:00", "Europe/Rome", "2025-10-01T00:00:00-04:00"), /// /// Partial hours timezones From 2f0b4046f11788075e44c49834b8e4d6bd1340e6 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 5 Nov 2025 17:18:32 +0000 Subject: [PATCH 38/54] [CI] Auto commit changes from spotless --- .../xpack/esql/expression/function/TestCaseSupplier.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java index e2b7ab4acd921..6d9f637b402dc 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/TestCaseSupplier.java @@ -47,7 +47,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.BinaryOperator; import java.util.function.Consumer; @@ -1195,8 +1194,6 @@ public static List dateCases(long min, long max) { ); } - - return cases; } From 096d9b3e94aa785637893f1508f12ee3e3c9958e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Thu, 6 Nov 2025 16:40:46 +0100 Subject: [PATCH 39/54] Add tests for every combination of cases --- .../function/scalar/date/DateTruncTests.java | 119 +++++++++++------- 1 file changed, 77 insertions(+), 42 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 933ab3cc5e010..3946fbf54fd34 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -22,11 +23,13 @@ import java.time.Duration; import java.time.Instant; +import java.time.LocalDateTime; import java.time.Period; import java.time.ZoneId; import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; +import java.util.stream.Stream; import static org.elasticsearch.test.ReadableMatchers.matchesDateMillis; import static org.elasticsearch.test.ReadableMatchers.matchesDateNanos; @@ -150,52 +153,84 @@ public static List makeTruncDurationTestCases() { } public static List makeTruncPeriodTestCases() { - String ts = "2023-02-17T10:25:33.38Z"; - return List.of( - /// - /// UTC - /// - new PeriodTestCaseData(Period.ofDays(1), ts, "UTC", "2023-02-17T00:00:00.00Z"), - new PeriodTestCaseData(Period.ofMonths(1), ts, "UTC", "2023-02-01T00:00:00.00Z"), - new PeriodTestCaseData(Period.ofYears(1), ts, "UTC", "2023-01-01T00:00:00.00Z"), - new PeriodTestCaseData(Period.ofDays(10), ts, "UTC", "2023-02-12T00:00:00.00Z"), - // 7 days period should return weekly rounding - new PeriodTestCaseData(Period.ofDays(7), ts, "UTC", "2023-02-13T00:00:00.00Z"), - // 3 months period should return quarterly - new PeriodTestCaseData(Period.ofMonths(3), ts, "UTC", "2023-01-01T00:00:00.00Z"), - // arbitrary period of months and years - new PeriodTestCaseData(Period.ofMonths(7), ts, "UTC", "2022-11-01T00:00:00.00Z"), - new PeriodTestCaseData(Period.ofYears(5), ts, "UTC", "2021-01-01T00:00:00.00Z"), - - /// - /// Timezones - /// - new PeriodTestCaseData(Period.ofDays(1), "2024-03-01T00:30:00Z", "-03", "2024-02-29T03:00:00Z"), + List cases = new ArrayList<>(); + + // Add generic cases for either UTC, fixed timezones and timezones with minutes. + // Note that we can't do this with variable timezones, as date formats accept only offsets. + // + // For every unit, we test 2 cases: 1 unit, and multiple units. + // Then, for every case, we check 4 boundaries (↑Bucket1, ↓Bucket2, ↑Bucket2, ↓Bucket3) to ensure the exact size of the buckets. + final List timezones = List.of("Z", "-08:00", "+04:00", "+11:45", "Europe/Madrid", "America/New_York"); + Stream.of( + // Days + new PeriodTestCaseData(Period.ofDays(1), "2023-02-16T23:59:59.99", "", "2023-02-16T00:00:00"), + new PeriodTestCaseData(Period.ofDays(1), "2023-02-17T00:00:00", "", "2023-02-17T00:00:00"), + new PeriodTestCaseData(Period.ofDays(1), "2023-02-17T23:59:59.99", "", "2023-02-17T00:00:00"), + new PeriodTestCaseData(Period.ofDays(1), "2023-02-18T00:00:00", "", "2023-02-18T00:00:00"), + new PeriodTestCaseData(Period.ofDays(10), "2023-02-11T23:59:59.99", "", "2023-02-02T00:00:00"), + new PeriodTestCaseData(Period.ofDays(10), "2023-02-12T00:00:00", "", "2023-02-12T00:00:00"), + new PeriodTestCaseData(Period.ofDays(10), "2023-02-21T23:59:59.99", "", "2023-02-12T00:00:00"), + new PeriodTestCaseData(Period.ofDays(10), "2023-02-22T00:00:00", "", "2023-02-22T00:00:00"), + // Weeks + new PeriodTestCaseData(Period.ofDays(7), "2023-02-05T23:59:59.99", "", "2023-01-30T00:00:00"), + new PeriodTestCaseData(Period.ofDays(7), "2023-02-06T00:00:00", "", "2023-02-06T00:00:00"), + new PeriodTestCaseData(Period.ofDays(7), "2023-02-12T23:59:59.99", "", "2023-02-06T00:00:00"), + new PeriodTestCaseData(Period.ofDays(7), "2023-02-13T00:00:00", "", "2023-02-13T00:00:00"), + new PeriodTestCaseData(Period.ofDays(21), "2023-01-25T23:59:59.99", "", "2023-01-05T00:00:00"), + new PeriodTestCaseData(Period.ofDays(21), "2023-01-26T00:00:00", "", "2023-01-26T00:00:00"), + new PeriodTestCaseData(Period.ofDays(21), "2023-02-15T23:59:59.99", "", "2023-01-26T00:00:00"), + new PeriodTestCaseData(Period.ofDays(21), "2023-02-16T00:00:00", "", "2023-02-16T00:00:00"), + // Months + new PeriodTestCaseData(Period.ofMonths(1), "2024-02-29T23:59:59.99", "", "2024-02-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(1), "2024-03-01T00:00:00", "", "2024-03-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(1), "2024-03-31T23:59:59.99", "", "2024-03-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(1), "2024-04-01T00:00:00", "", "2024-04-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(7), "2022-10-31T23:59:59.99", "", "2022-04-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(7), "2022-11-01T00:00:00", "", "2022-11-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(7), "2023-05-31T23:59:59.99", "", "2022-11-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(7), "2023-06-01T00:00:00", "", "2023-06-01T00:00:00"), + // Quarters + new PeriodTestCaseData(Period.ofMonths(3), "2023-12-31T23:59:59.99", "", "2023-10-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(3), "2024-01-01T00:00:00", "", "2024-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(3), "2024-03-31T23:59:59.99", "", "2024-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(3), "2024-04-01T00:00:00", "", "2024-04-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(6), "2023-12-31T23:59:59.99", "", "2023-07-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(6), "2024-01-01T00:00:00", "", "2024-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(6), "2024-06-30T23:59:59.99", "", "2024-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofMonths(6), "2024-07-01T00:00:00", "", "2024-07-01T00:00:00"), + // Years + new PeriodTestCaseData(Period.ofYears(1), "2022-12-31T23:59:59.99", "", "2022-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofYears(1), "2023-01-01T00:00:00", "", "2023-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofYears(1), "2023-12-31T23:59:59.99", "", "2023-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofYears(1), "2024-01-01T00:00:00", "", "2024-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofYears(5), "2020-12-31T23:59:59.99", "", "2016-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofYears(5), "2021-01-01T00:00:00", "", "2021-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofYears(5), "2025-12-31T23:59:59.99", "", "2021-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofYears(5), "2026-01-01T00:00:00", "", "2026-01-01T00:00:00") + ).forEach(c -> timezones.forEach(timezone -> { + // Convert the timezone to the offset in each local time. + // This is required as date strings can't have a zone name as its zone. + var inputOffset = timezone.startsWith("+") || timezone.startsWith("-") + ? timezone + : LocalDateTime.parse(c.inputDate()).atZone(ZoneId.of(timezone)).getOffset().getId(); + var expectedOffset = timezone.startsWith("+") || timezone.startsWith("-") + ? timezone + : LocalDateTime.parse(c.expectedDate()).atZone(ZoneId.of(timezone)).getOffset().getId(); + cases.add( + new PeriodTestCaseData(c.period(), c.inputDate() + inputOffset, timezone, c.expectedDate() + expectedOffset) + ); + })); + // Special cases + cases.addAll(List.of( /// - /// Timezone with DST (e.g. New York: -5 to -4 at 2025-03-09T02:00:00-05, and -4 to -5 at 2025-11-02T02:00:00-04) + /// DST boundaries (e.g. New York: -5 to -4 at 2025-03-09T02:00:00-05, and -4 to -5 at 2025-11-02T02:00:00-04) /// + // Days new PeriodTestCaseData(Period.ofDays(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-09T00:00:00-05:00"), - new PeriodTestCaseData(Period.ofMonths(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-01T00:00:00-05:00"), - new PeriodTestCaseData(Period.ofYears(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-01-01T00:00:00-05:00"), - new PeriodTestCaseData(Period.ofDays(10), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-03T00:00:00-05:00"), - new PeriodTestCaseData(Period.ofDays(1), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-11-02T00:00:00-04:00"), - new PeriodTestCaseData(Period.ofDays(7), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-10-27T00:00:00-04:00"), - new PeriodTestCaseData(Period.ofMonths(3), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-10-01T00:00:00-04:00"), - new PeriodTestCaseData(Period.ofDays(1), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), - new PeriodTestCaseData(Period.ofMonths(3), "2025-11-02T05:00:00-05:00", "Europe/Rome", "2025-10-01T00:00:00-04:00"), - - /// - /// Partial hours timezones - /// - new PeriodTestCaseData(Period.ofDays(1), "2025-03-09T07:08:09-05:45", "-05:45", "2025-03-09T00:00:00-05:45"), - new PeriodTestCaseData(Period.ofMonths(1), "2025-03-09T07:08:09-05:45", "-05:45", "2025-03-01T00:00:00-05:45"), - new PeriodTestCaseData(Period.ofYears(1), "2025-03-09T07:08:09-05:45", "-05:45", "2025-01-01T00:00:00-05:45"), - new PeriodTestCaseData(Period.ofDays(10), "2025-03-09T07:08:09-05:45", "-05:45", "2025-03-03T00:00:00-05:45"), - new PeriodTestCaseData(Period.ofDays(1), "2025-03-09T07:08:09-05:45", "-05:45", "2025-03-09T00:00:00-05:45"), - new PeriodTestCaseData(Period.ofDays(7), "2025-03-09T07:08:09-05:45", "-05:45", "2025-03-03T00:00:00-05:45"), - new PeriodTestCaseData(Period.ofMonths(3), "2025-03-09T07:08:09-05:45", "-05:45", "2025-01-01T00:00:00-05:45") - ); + new PeriodTestCaseData(Period.ofDays(1), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-11-02T00:00:00-04:00") + )); + return cases; } private static List ofDatePeriod(PeriodTestCaseData data) { From 4dc641f2a7336e4ce2f4284508ca7ce754a6ab35 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 6 Nov 2025 15:48:33 +0000 Subject: [PATCH 40/54] [CI] Auto commit changes from spotless --- .../function/scalar/date/DateTruncTests.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 3946fbf54fd34..d01cc5b608842 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -12,7 +12,6 @@ import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.core.Nullable; -import org.elasticsearch.core.Tuple; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -216,20 +215,20 @@ public static List makeTruncPeriodTestCases() { var expectedOffset = timezone.startsWith("+") || timezone.startsWith("-") ? timezone : LocalDateTime.parse(c.expectedDate()).atZone(ZoneId.of(timezone)).getOffset().getId(); - cases.add( - new PeriodTestCaseData(c.period(), c.inputDate() + inputOffset, timezone, c.expectedDate() + expectedOffset) - ); + cases.add(new PeriodTestCaseData(c.period(), c.inputDate() + inputOffset, timezone, c.expectedDate() + expectedOffset)); })); // Special cases - cases.addAll(List.of( - /// + cases.addAll( + List.of( + /// /// DST boundaries (e.g. New York: -5 to -4 at 2025-03-09T02:00:00-05, and -4 to -5 at 2025-11-02T02:00:00-04) /// - // Days - new PeriodTestCaseData(Period.ofDays(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-09T00:00:00-05:00"), - new PeriodTestCaseData(Period.ofDays(1), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-11-02T00:00:00-04:00") - )); + // Days + new PeriodTestCaseData(Period.ofDays(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-09T00:00:00-05:00"), + new PeriodTestCaseData(Period.ofDays(1), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-11-02T00:00:00-04:00") + ) + ); return cases; } From d46fc5b9bbfeb2f3bcab9714f30a5fe4c7a0348e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Thu, 6 Nov 2025 20:11:11 +0100 Subject: [PATCH 41/54] Fixed date intervals with +1 units, and added negative years tests --- .../org/elasticsearch/common/Rounding.java | 20 +++--- .../common/time/LocalDateTimeUtils.java | 27 ++++++++ .../common/time/LocalDateTimeUtilsTests.java | 65 +++++++++++++++++++ .../function/grouping/BucketTests.java | 12 +--- .../function/grouping/TBucketTests.java | 12 +--- .../function/scalar/date/DateTruncTests.java | 23 ++++++- 6 files changed, 125 insertions(+), 34 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/common/time/LocalDateTimeUtils.java create mode 100644 server/src/test/java/org/elasticsearch/common/time/LocalDateTimeUtilsTests.java diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 4759926eeebaf..2a82fd7f0a169 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.time.DateUtils; +import org.elasticsearch.common.time.LocalDateTimeUtils; import org.elasticsearch.core.TimeValue; import java.io.IOException; @@ -29,7 +30,6 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.temporal.ChronoField; -import java.time.temporal.ChronoUnit; import java.time.temporal.IsoFields; import java.time.temporal.TemporalField; import java.time.temporal.TemporalQueries; @@ -546,16 +546,16 @@ private LocalDateTime truncateLocalDateTime(LocalDateTime localDateTime) { return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonthValue(), 1, 0, 0); case QUARTER_OF_YEAR: - return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonth().firstMonthOfQuarter(), 1, 0, 0); + return LocalDateTime.of(localDateTime.getYear(), (((localDateTime.getMonthValue() - 1) / 3) * 3) + 1, 1, 0, 0); case YEAR_OF_CENTURY: return LocalDateTime.of(LocalDate.of(localDateTime.getYear(), 1, 1), LocalTime.MIDNIGHT); case YEARS_OF_CENTURY: - return LocalDateTime.of(LocalDate.of(localDateTime.getYear(), 1, 1), LocalTime.MIDNIGHT); + return LocalDateTimeUtils.truncateToYears(localDateTime, multiplier); case MONTHS_OF_YEAR: - return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonthValue(), 1, 0, 0); + return LocalDateTimeUtils.truncateToMonths(localDateTime, multiplier); default: throw new IllegalArgumentException("NOT YET IMPLEMENTED for unit " + unit); @@ -914,13 +914,11 @@ private LocalDateTime nextRelevantMidnight(LocalDateTime localMidnight) { assert localMidnight.toLocalTime().equals(LocalTime.MIDNIGHT) : "nextRelevantMidnight should only be called at midnight"; return switch (unit) { - case DAY_OF_MONTH -> localMidnight.plus(1, ChronoUnit.DAYS); - case WEEK_OF_WEEKYEAR -> localMidnight.plus(7, ChronoUnit.DAYS); - case MONTH_OF_YEAR -> localMidnight.plus(1, ChronoUnit.MONTHS); - case QUARTER_OF_YEAR -> localMidnight.plus(3, ChronoUnit.MONTHS); - case YEAR_OF_CENTURY -> localMidnight.plus(1, ChronoUnit.YEARS); - case YEARS_OF_CENTURY -> localMidnight.plus(1, ChronoUnit.YEARS); - case MONTHS_OF_YEAR -> localMidnight.plus(1, ChronoUnit.MONTHS); + case DAY_OF_MONTH -> localMidnight.plusDays(multiplier); + case WEEK_OF_WEEKYEAR -> localMidnight.plusDays(7L * multiplier); + case MONTH_OF_YEAR, MONTHS_OF_YEAR -> localMidnight.plusMonths(multiplier); + case QUARTER_OF_YEAR -> localMidnight.plusMonths(3L * multiplier); + case YEAR_OF_CENTURY, YEARS_OF_CENTURY -> localMidnight.plusYears(multiplier); default -> throw new IllegalArgumentException("Unknown round-to-midnight unit: " + unit); }; } diff --git a/server/src/main/java/org/elasticsearch/common/time/LocalDateTimeUtils.java b/server/src/main/java/org/elasticsearch/common/time/LocalDateTimeUtils.java new file mode 100644 index 0000000000000..7c68e374344c2 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/time/LocalDateTimeUtils.java @@ -0,0 +1,27 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.common.time; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public class LocalDateTimeUtils { + public static LocalDateTime truncateToMonths(LocalDateTime dateTime, int months) { + int totalMonths = (dateTime.getYear() - 1) * 12 + dateTime.getMonthValue() - 1; + int truncatedMonths = Math.floorDiv(totalMonths, months) * months; + return LocalDateTime.of(LocalDate.of(truncatedMonths / 12 + 1, truncatedMonths % 12 + 1, 1), LocalTime.MIDNIGHT); + } + + public static LocalDateTime truncateToYears(LocalDateTime dateTime, int years) { + int truncatedYear = Math.floorDiv(dateTime.getYear() - 1, years) * years + 1; + return LocalDateTime.of(LocalDate.of(truncatedYear, 1, 1), LocalTime.MIDNIGHT); + } +} diff --git a/server/src/test/java/org/elasticsearch/common/time/LocalDateTimeUtilsTests.java b/server/src/test/java/org/elasticsearch/common/time/LocalDateTimeUtilsTests.java new file mode 100644 index 0000000000000..464a724205da6 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/time/LocalDateTimeUtilsTests.java @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.common.time; + +import org.elasticsearch.test.ESTestCase; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.Year; + +import static org.hamcrest.Matchers.equalTo; + +public class LocalDateTimeUtilsTests extends ESTestCase { + public void testTruncateToYears() { + int randomYear = randomIntBetween(1, 3000); + assertTruncateToYears(1, randomYear, randomYear); + + assertTruncateToYears(10, 1, 1); + assertTruncateToYears(10, 11, 11); + assertTruncateToYears(10, 10, 1); + assertTruncateToYears(10, 11, 11); + assertTruncateToYears(10, 2015, 2011); + + assertTruncateToYears(4, 2000, 1997); + assertTruncateToYears(4, 2003, 2001); + assertTruncateToYears(4, 2004, 2001); + assertTruncateToYears(4, 2005, 2005); + + assertTruncateToYears(4, 1, 1); + assertTruncateToYears(4, -1, -3); + assertTruncateToYears(4, -3, -3); + assertTruncateToYears(4, -4, -7); + } + + private void assertTruncateToYears(int interval, int year, int expectedYear) { + int inputMonth = randomIntBetween(1, 12); + LocalDateTime inputDate = LocalDateTime.of( + year, + inputMonth, + Month.of(inputMonth).length(Year.isLeap(year)), + randomIntBetween(0, 23), + randomIntBetween(0, 59), + randomIntBetween(0, 59) + ); + + LocalDateTime expectedResult = LocalDateTime.of(LocalDate.of(expectedYear, 1, 1), LocalTime.MIDNIGHT); + + LocalDateTime resultYears = LocalDateTimeUtils.truncateToYears(inputDate, interval); + assertThat(resultYears, equalTo(expectedResult)); + + // Also tests the same with months + LocalDateTime resultMonths = LocalDateTimeUtils.truncateToMonths(inputDate, interval * 12); + assertThat(resultMonths, equalTo(expectedResult)); + } + +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java index 0d71b067651c1..45db2e9a5792f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java @@ -162,11 +162,7 @@ private static void dateTruncCases(List suppliers) { List.of(DataType.DATE_NANOS, DataType.DATE_PERIOD), () -> new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData( - DateUtils.toNanoSeconds(data.inputDateAsMillis()), - DataType.DATE_NANOS, - "field" - ), + new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "field"), new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral() ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), @@ -199,11 +195,7 @@ private static void dateTruncCases(List suppliers) { List.of(DataType.DATE_NANOS, DataType.TIME_DURATION), () -> new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData( - DateUtils.toNanoSeconds(data.inputDateAsMillis()), - DataType.DATE_NANOS, - "field" - ), + new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "field"), new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral() ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java index e52afe5c0c2fa..0d04118771661 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java @@ -148,11 +148,7 @@ private static void dateTruncCases(List suppliers) { () -> new TestCaseSupplier.TestCase( List.of( new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), - new TestCaseSupplier.TypedData( - DateUtils.toNanoSeconds(data.inputDateAsMillis()), - DataType.DATE_NANOS, - "@timestamp" - ) + new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "@timestamp") ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, @@ -185,11 +181,7 @@ private static void dateTruncCases(List suppliers) { () -> new TestCaseSupplier.TestCase( List.of( new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), - new TestCaseSupplier.TypedData( - DateUtils.toNanoSeconds(data.inputDateAsMillis()), - DataType.DATE_NANOS, - "@timestamp" - ) + new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "@timestamp") ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index d01cc5b608842..9b325c6f1fb34 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -74,6 +74,12 @@ public ZoneId zoneId() { public long inputDateAsMillis() { return Instant.parse(inputDate).toEpochMilli(); } + + public long inputDateAsNanos() { + long millis = inputDateAsMillis(); + assumeTrue("Before epoch dates can't be converted to nanos", millis >= 0); + return DateUtils.toNanoSeconds(inputDateAsMillis()); + } } public record DurationTestCaseData(Duration duration, String inputDate, @Nullable String zoneIdString, String expectedDate) { @@ -94,6 +100,12 @@ public ZoneId zoneId() { public long inputDateAsMillis() { return Instant.parse(inputDate).toEpochMilli(); } + + public long inputDateAsNanos() { + long millis = inputDateAsMillis(); + assumeTrue("Before epoch dates can't be converted to nanos", millis >= 0); + return DateUtils.toNanoSeconds(inputDateAsMillis()); + } } public static List makeTruncDurationTestCases() { @@ -205,7 +217,12 @@ public static List makeTruncPeriodTestCases() { new PeriodTestCaseData(Period.ofYears(5), "2020-12-31T23:59:59.99", "", "2016-01-01T00:00:00"), new PeriodTestCaseData(Period.ofYears(5), "2021-01-01T00:00:00", "", "2021-01-01T00:00:00"), new PeriodTestCaseData(Period.ofYears(5), "2025-12-31T23:59:59.99", "", "2021-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofYears(5), "2026-01-01T00:00:00", "", "2026-01-01T00:00:00") + new PeriodTestCaseData(Period.ofYears(5), "2026-01-01T00:00:00", "", "2026-01-01T00:00:00"), + // Negative years + new PeriodTestCaseData(Period.ofYears(4), "-0004-12-31T23:59:59.99", "", "-0007-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofYears(4), "-0003-01-01T00:00:00", "", "-0003-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofYears(4), "-0001-12-31T23:59:59.99", "", "-0003-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofYears(4), "0001-01-01T00:00:00", "", "0001-01-01T00:00:00") ).forEach(c -> timezones.forEach(timezone -> { // Convert the timezone to the offset in each local time. // This is required as date strings can't have a zone name as its zone. @@ -253,7 +270,7 @@ private static List ofDatePeriod(PeriodTestCaseData data) { () -> new TestCaseSupplier.TestCase( List.of( new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(data.inputDateAsMillis()), DataType.DATE_NANOS, "date") + new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, @@ -284,7 +301,7 @@ private static List ofDuration(DurationTestCaseData data) { () -> new TestCaseSupplier.TestCase( List.of( new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(DateUtils.toNanoSeconds(data.inputDateAsMillis()), DataType.DATE_NANOS, "date") + new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), DataType.DATE_NANOS, From bdd7138bfbf369be65998ec1dd9d5211cd2f951f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 7 Nov 2025 13:54:20 +0100 Subject: [PATCH 42/54] Updated duration tests to cover all timezone kinds --- .../function/scalar/date/DateTruncTests.java | 141 ++++++++++-------- 1 file changed, 78 insertions(+), 63 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 9b325c6f1fb34..e043a21a98d86 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -108,70 +108,86 @@ public long inputDateAsNanos() { } } + private static final List TEST_TIMEZONES = List.of("Z", "-08:00", "+14:00", "+11:45", "Europe/Madrid", "America/New_York"); + public static List makeTruncDurationTestCases() { + List cases = new ArrayList<>(); String ts = "2023-02-17T10:25:33.38Z"; - return List.of( - /// - /// Timezone agnostic (<=1m intervals) - /// - new DurationTestCaseData(Duration.ofMillis(100), ts, null, "2023-02-17T10:25:33.30Z"), - new DurationTestCaseData(Duration.ofSeconds(1), ts, null, "2023-02-17T10:25:33Z"), - new DurationTestCaseData(Duration.ofMinutes(1), ts, null, "2023-02-17T10:25:00Z"), - new DurationTestCaseData(Duration.ofSeconds(30), ts, null, "2023-02-17T10:25:30Z"), - - /// - /// Timezone dependent (>1m intervals) - /// - new DurationTestCaseData(Duration.ofHours(1), ts, "UTC", "2023-02-17T10:00:00Z"), - new DurationTestCaseData(Duration.ofMinutes(15), ts, "UTC", "2023-02-17T10:15:00Z"), - new DurationTestCaseData(Duration.ofHours(3), ts, "UTC", "2023-02-17T09:00:00Z"), - new DurationTestCaseData(Duration.ofHours(3), ts, "-02:00", "2023-02-17T08:00:00Z"), - new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "UTC", "2020-01-01T03:00:00Z"), - new DurationTestCaseData(Duration.ofHours(3), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z"), - new DurationTestCaseData(Duration.ofMinutes(3 * 60), "2020-01-01T05:30:00Z", "+01:00", "2020-01-01T05:00:00Z"), - new DurationTestCaseData(Duration.ofHours(6), "2024-03-01T00:30:00Z", "-03", "2024-02-29T21:00:00Z"), - new DurationTestCaseData(Duration.ofHours(24), "2024-03-01T00:30:00Z", "-03", "2024-02-29T03:00:00Z"), - new DurationTestCaseData(Duration.ofHours(5), "2020-01-01T05:30:00Z", "+01", "2020-01-01T01:00:00Z"), - - /// - /// Daylight savings - /// - // - +1 -> +2 at 2025-03-30T02:00:00+01:00 - new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T00:00:00+01:00", "Europe/Paris", "2025-03-30T00:00:00+01:00"), - new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T01:00:00+01:00", "Europe/Paris", "2025-03-30T00:00:00+01:00"), - new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T03:00:00+02:00", "Europe/Paris", "2025-03-30T03:00:00+02:00"), - new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T04:00:00+02:00", "Europe/Paris", "2025-03-30T03:00:00+02:00"), - // - +2 -> +1 at 2025-10-26T03:00:00+02:00 - new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T01:00:00+02:00", "Europe/Paris", "2025-10-26T00:00:00+02:00"), - new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T02:00:00+02:00", "Europe/Paris", "2025-10-26T00:00:00+02:00"), - new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T02:00:00+01:00", "Europe/Paris", "2025-10-26T00:00:00+02:00"), - new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T03:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), - new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T04:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), - // Bigger intervals - new DurationTestCaseData(Duration.ofHours(12), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), - new DurationTestCaseData(Duration.ofHours(24), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), - new DurationTestCaseData(Duration.ofHours(48), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-25T00:00:00+02:00"), - - /// - /// Partial hours timezones - /// - new DurationTestCaseData(Duration.ofMinutes(1), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T02:09:00+01:15"), - new DurationTestCaseData(Duration.ofMinutes(30), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T02:00:00+01:15"), - new DurationTestCaseData(Duration.ofHours(1), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T02:00:00+01:15"), - new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T00:00:00+01:15"), - new DurationTestCaseData(Duration.ofHours(24), "2025-10-26T02:09:09+01:15", "+01:15", "2025-10-26T00:00:00+01:15") + + // Add generic cases for either UTC, fixed timezones and timezones with minutes. + // + // For every unit, we test 2 cases: 1 unit, and multiple units. + // Then, for every case, we check 4 boundaries (↑Bucket1, ↓Bucket2, ↑Bucket2, ↓Bucket3) to ensure the exact size of the buckets. + Stream.of( + // Milliseconds + new DurationTestCaseData(Duration.ofMillis(1), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:33.385"), + new DurationTestCaseData(Duration.ofMillis(10), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:33.38"), + new DurationTestCaseData(Duration.ofMillis(100), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:33.3"), + new DurationTestCaseData(Duration.ofMillis(1000), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:33"), + new DurationTestCaseData(Duration.ofMillis(13), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:33.384"), + new DurationTestCaseData(Duration.ofMillis(13), "2023-02-17T10:25:33.399", "", "2023-02-17T10:25:33.397"), + // Seconds + new DurationTestCaseData(Duration.ofSeconds(1), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:33"), + new DurationTestCaseData(Duration.ofSeconds(10), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:30"), + new DurationTestCaseData(Duration.ofSeconds(60), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:00"), + new DurationTestCaseData(Duration.ofSeconds(300), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:00"), + new DurationTestCaseData(Duration.ofSeconds(3600), "2023-02-17T10:25:33.385", "", "2023-02-17T10:00:00"), + // Minutes + new DurationTestCaseData(Duration.ofMinutes(1), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:00"), + new DurationTestCaseData(Duration.ofMinutes(5), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:00"), + new DurationTestCaseData(Duration.ofMinutes(60), "2023-02-17T10:25:33.385", "", "2023-02-17T10:00:00"), + // Hours + new DurationTestCaseData(Duration.ofHours(1), "2023-02-17T10:25:33.385", "", "2023-02-17T10:00:00"), + new DurationTestCaseData(Duration.ofHours(3), "2023-02-17T10:25:33.385", "", "2023-02-17T09:00:00"), + new DurationTestCaseData(Duration.ofHours(6), "2023-02-17T10:25:33.385", "", "2023-02-17T06:00:00"), + new DurationTestCaseData(Duration.ofHours(5), "2023-02-17T09:59:59.999", "", "2023-02-17T05:00:00"), + new DurationTestCaseData(Duration.ofHours(5), "2023-02-17T10:25:33.385", "", "2023-02-17T10:00:00"), + new DurationTestCaseData(Duration.ofHours(24), "2023-02-17T10:25:33.385", "", "2023-02-17T00:00:00"), + new DurationTestCaseData(Duration.ofHours(48), "2023-02-17T10:25:33.385", "", "2023-02-16T00:00:00") + ).forEach(c -> TEST_TIMEZONES.forEach(timezone -> { + // Convert the timezone to the offset in each local time. + // This is required as date strings can't have a zone name as its zone. + var inputOffset = timezoneToOffset(timezone, c.inputDate()); + var expectedOffset = timezoneToOffset(timezone, c.expectedDate()); + cases.add(new DurationTestCaseData(c.duration(), c.inputDate() + inputOffset, timezone, c.expectedDate() + expectedOffset)); + })); + + cases.addAll( + List.of( + // Timezone agnostic (<=1m intervals, "null" for randomized timezones) + new DurationTestCaseData(Duration.ofMillis(100), ts, null, "2023-02-17T10:25:33.30Z"), + new DurationTestCaseData(Duration.ofSeconds(1), ts, null, "2023-02-17T10:25:33Z"), + new DurationTestCaseData(Duration.ofMinutes(1), ts, null, "2023-02-17T10:25:00Z"), + new DurationTestCaseData(Duration.ofSeconds(30), ts, null, "2023-02-17T10:25:30Z"), + + // Daylight savings boundaries + // - +1 -> +2 at 2025-03-30T02:00:00+01:00 + new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T00:00:00+01:00", "Europe/Paris", "2025-03-30T00:00:00+01:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T01:00:00+01:00", "Europe/Paris", "2025-03-30T00:00:00+01:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T03:00:00+02:00", "Europe/Paris", "2025-03-30T03:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-03-30T04:00:00+02:00", "Europe/Paris", "2025-03-30T03:00:00+02:00"), + // - +2 -> +1 at 2025-10-26T03:00:00+02:00 + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T01:00:00+02:00", "Europe/Paris", "2025-10-26T00:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T02:00:00+02:00", "Europe/Paris", "2025-10-26T00:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T02:00:00+01:00", "Europe/Paris", "2025-10-26T00:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T03:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), + new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T04:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), + // Bigger intervals + new DurationTestCaseData(Duration.ofHours(12), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(24), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), + new DurationTestCaseData(Duration.ofHours(48), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-25T00:00:00+02:00") + ) ); + return cases; } public static List makeTruncPeriodTestCases() { List cases = new ArrayList<>(); // Add generic cases for either UTC, fixed timezones and timezones with minutes. - // Note that we can't do this with variable timezones, as date formats accept only offsets. // // For every unit, we test 2 cases: 1 unit, and multiple units. // Then, for every case, we check 4 boundaries (↑Bucket1, ↓Bucket2, ↑Bucket2, ↓Bucket3) to ensure the exact size of the buckets. - final List timezones = List.of("Z", "-08:00", "+04:00", "+11:45", "Europe/Madrid", "America/New_York"); Stream.of( // Days new PeriodTestCaseData(Period.ofDays(1), "2023-02-16T23:59:59.99", "", "2023-02-16T00:00:00"), @@ -223,25 +239,18 @@ public static List makeTruncPeriodTestCases() { new PeriodTestCaseData(Period.ofYears(4), "-0003-01-01T00:00:00", "", "-0003-01-01T00:00:00"), new PeriodTestCaseData(Period.ofYears(4), "-0001-12-31T23:59:59.99", "", "-0003-01-01T00:00:00"), new PeriodTestCaseData(Period.ofYears(4), "0001-01-01T00:00:00", "", "0001-01-01T00:00:00") - ).forEach(c -> timezones.forEach(timezone -> { + ).forEach(c -> TEST_TIMEZONES.forEach(timezone -> { // Convert the timezone to the offset in each local time. // This is required as date strings can't have a zone name as its zone. - var inputOffset = timezone.startsWith("+") || timezone.startsWith("-") - ? timezone - : LocalDateTime.parse(c.inputDate()).atZone(ZoneId.of(timezone)).getOffset().getId(); - var expectedOffset = timezone.startsWith("+") || timezone.startsWith("-") - ? timezone - : LocalDateTime.parse(c.expectedDate()).atZone(ZoneId.of(timezone)).getOffset().getId(); + var inputOffset = timezoneToOffset(timezone, c.inputDate()); + var expectedOffset = timezoneToOffset(timezone, c.expectedDate()); cases.add(new PeriodTestCaseData(c.period(), c.inputDate() + inputOffset, timezone, c.expectedDate() + expectedOffset)); })); // Special cases cases.addAll( List.of( - /// - /// DST boundaries (e.g. New York: -5 to -4 at 2025-03-09T02:00:00-05, and -4 to -5 at 2025-11-02T02:00:00-04) - /// - // Days + // DST boundaries (e.g. New York: -5 to -4 at 2025-03-09T02:00:00-05, and -4 to -5 at 2025-11-02T02:00:00-04) new PeriodTestCaseData(Period.ofDays(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-09T00:00:00-05:00"), new PeriodTestCaseData(Period.ofDays(1), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-11-02T00:00:00-04:00") ) @@ -249,6 +258,12 @@ public static List makeTruncPeriodTestCases() { return cases; } + private static String timezoneToOffset(String timezone, String date) { + return timezone.startsWith("+") || timezone.startsWith("-") + ? timezone + : LocalDateTime.parse(date).atZone(ZoneId.of(timezone)).getOffset().getId(); + } + private static List ofDatePeriod(PeriodTestCaseData data) { return List.of( new TestCaseSupplier( From 55c4efdc16dd399d7b0cfaf71bc42c1fbd043872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 7 Nov 2025 14:38:59 +0100 Subject: [PATCH 43/54] Reorganized tests --- .../function/scalar/date/DateTruncTests.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index e043a21a98d86..e605c0b477daf 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -48,23 +48,23 @@ public DateTruncTests(@Name("TestCase") Supplier test public static Iterable parameters() { List suppliers = new ArrayList<>(); - makeTruncPeriodTestCases().stream().map(DateTruncTests::ofDatePeriod).forEach(suppliers::addAll); makeTruncDurationTestCases().stream().map(DateTruncTests::ofDuration).forEach(suppliers::addAll); + makeTruncPeriodTestCases().stream().map(DateTruncTests::ofDatePeriod).forEach(suppliers::addAll); suppliers.add(randomSecond()); return parameterSuppliersFromTypedDataWithDefaultChecks(true, suppliers); } - public record PeriodTestCaseData(Period period, String inputDate, @Nullable String zoneIdString, String expectedDate) { + public record DurationTestCaseData(Duration duration, String inputDate, @Nullable String zoneIdString, String expectedDate) { public String testCaseNameForMillis() { var zoneIdName = zoneIdString == null ? "random" : zoneIdString; - return "period, millis; " + period + ", " + zoneIdName + ", " + inputDate; + return "duration, millis; " + duration + ", " + zoneIdName + ", " + inputDate; } public String testCaseNameForNanos() { var zoneIdName = zoneIdString == null ? "random" : zoneIdString; - return "period, nanos; " + period + ", " + zoneIdName + ", " + inputDate; + return "duration, nanos; " + duration + ", " + zoneIdName + ", " + inputDate; } public ZoneId zoneId() { @@ -82,15 +82,15 @@ public long inputDateAsNanos() { } } - public record DurationTestCaseData(Duration duration, String inputDate, @Nullable String zoneIdString, String expectedDate) { + public record PeriodTestCaseData(Period period, String inputDate, @Nullable String zoneIdString, String expectedDate) { public String testCaseNameForMillis() { var zoneIdName = zoneIdString == null ? "random" : zoneIdString; - return "duration, millis; " + duration + ", " + zoneIdName + ", " + inputDate; + return "period, millis; " + period + ", " + zoneIdName + ", " + inputDate; } public String testCaseNameForNanos() { var zoneIdName = zoneIdString == null ? "random" : zoneIdString; - return "duration, nanos; " + duration + ", " + zoneIdName + ", " + inputDate; + return "period, nanos; " + period + ", " + zoneIdName + ", " + inputDate; } public ZoneId zoneId() { @@ -264,14 +264,14 @@ private static String timezoneToOffset(String timezone, String date) { : LocalDateTime.parse(date).atZone(ZoneId.of(timezone)).getOffset().getId(); } - private static List ofDatePeriod(PeriodTestCaseData data) { + private static List ofDuration(DurationTestCaseData data) { return List.of( new TestCaseSupplier( data.testCaseNameForMillis(), - List.of(DataType.DATE_PERIOD, DataType.DATETIME), + List.of(DataType.TIME_DURATION, DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "date") ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), @@ -281,10 +281,10 @@ private static List ofDatePeriod(PeriodTestCaseData data) { ), new TestCaseSupplier( data.testCaseNameForNanos(), - List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), + List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), @@ -295,14 +295,14 @@ private static List ofDatePeriod(PeriodTestCaseData data) { ); } - private static List ofDuration(DurationTestCaseData data) { + private static List ofDatePeriod(PeriodTestCaseData data) { return List.of( new TestCaseSupplier( data.testCaseNameForMillis(), - List.of(DataType.TIME_DURATION, DataType.DATETIME), + List.of(DataType.DATE_PERIOD, DataType.DATETIME), () -> new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "date") ), Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), @@ -312,10 +312,10 @@ private static List ofDuration(DurationTestCaseData data) { ), new TestCaseSupplier( data.testCaseNameForNanos(), - List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), + List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), () -> new TestCaseSupplier.TestCase( List.of( - new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") ), Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), From d25ab575e7d4aaf49b57792f116f20b17569fe48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 7 Nov 2025 17:25:19 +0100 Subject: [PATCH 44/54] Remove redundant test cases --- .../function/scalar/date/DateTruncTests.java | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index e605c0b477daf..10e8bb67e1db9 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -117,7 +117,7 @@ public static List makeTruncDurationTestCases() { // Add generic cases for either UTC, fixed timezones and timezones with minutes. // // For every unit, we test 2 cases: 1 unit, and multiple units. - // Then, for every case, we check 4 boundaries (↑Bucket1, ↓Bucket2, ↑Bucket2, ↓Bucket3) to ensure the exact size of the buckets. + // Then, for every case, we check 2 boundaries (↑Bucket1, ↓Bucket2) to ensure the exact size of the buckets. Stream.of( // Milliseconds new DurationTestCaseData(Duration.ofMillis(1), "2023-02-17T10:25:33.385", "", "2023-02-17T10:25:33.385"), @@ -187,58 +187,36 @@ public static List makeTruncPeriodTestCases() { // Add generic cases for either UTC, fixed timezones and timezones with minutes. // // For every unit, we test 2 cases: 1 unit, and multiple units. - // Then, for every case, we check 4 boundaries (↑Bucket1, ↓Bucket2, ↑Bucket2, ↓Bucket3) to ensure the exact size of the buckets. + // Then, for every case, we check 2 boundaries (↑Bucket1, ↓Bucket2) to ensure the exact size of the buckets. Stream.of( // Days new PeriodTestCaseData(Period.ofDays(1), "2023-02-16T23:59:59.99", "", "2023-02-16T00:00:00"), new PeriodTestCaseData(Period.ofDays(1), "2023-02-17T00:00:00", "", "2023-02-17T00:00:00"), - new PeriodTestCaseData(Period.ofDays(1), "2023-02-17T23:59:59.99", "", "2023-02-17T00:00:00"), - new PeriodTestCaseData(Period.ofDays(1), "2023-02-18T00:00:00", "", "2023-02-18T00:00:00"), new PeriodTestCaseData(Period.ofDays(10), "2023-02-11T23:59:59.99", "", "2023-02-02T00:00:00"), new PeriodTestCaseData(Period.ofDays(10), "2023-02-12T00:00:00", "", "2023-02-12T00:00:00"), - new PeriodTestCaseData(Period.ofDays(10), "2023-02-21T23:59:59.99", "", "2023-02-12T00:00:00"), - new PeriodTestCaseData(Period.ofDays(10), "2023-02-22T00:00:00", "", "2023-02-22T00:00:00"), // Weeks new PeriodTestCaseData(Period.ofDays(7), "2023-02-05T23:59:59.99", "", "2023-01-30T00:00:00"), new PeriodTestCaseData(Period.ofDays(7), "2023-02-06T00:00:00", "", "2023-02-06T00:00:00"), - new PeriodTestCaseData(Period.ofDays(7), "2023-02-12T23:59:59.99", "", "2023-02-06T00:00:00"), - new PeriodTestCaseData(Period.ofDays(7), "2023-02-13T00:00:00", "", "2023-02-13T00:00:00"), new PeriodTestCaseData(Period.ofDays(21), "2023-01-25T23:59:59.99", "", "2023-01-05T00:00:00"), new PeriodTestCaseData(Period.ofDays(21), "2023-01-26T00:00:00", "", "2023-01-26T00:00:00"), - new PeriodTestCaseData(Period.ofDays(21), "2023-02-15T23:59:59.99", "", "2023-01-26T00:00:00"), - new PeriodTestCaseData(Period.ofDays(21), "2023-02-16T00:00:00", "", "2023-02-16T00:00:00"), // Months new PeriodTestCaseData(Period.ofMonths(1), "2024-02-29T23:59:59.99", "", "2024-02-01T00:00:00"), new PeriodTestCaseData(Period.ofMonths(1), "2024-03-01T00:00:00", "", "2024-03-01T00:00:00"), - new PeriodTestCaseData(Period.ofMonths(1), "2024-03-31T23:59:59.99", "", "2024-03-01T00:00:00"), - new PeriodTestCaseData(Period.ofMonths(1), "2024-04-01T00:00:00", "", "2024-04-01T00:00:00"), new PeriodTestCaseData(Period.ofMonths(7), "2022-10-31T23:59:59.99", "", "2022-04-01T00:00:00"), new PeriodTestCaseData(Period.ofMonths(7), "2022-11-01T00:00:00", "", "2022-11-01T00:00:00"), - new PeriodTestCaseData(Period.ofMonths(7), "2023-05-31T23:59:59.99", "", "2022-11-01T00:00:00"), - new PeriodTestCaseData(Period.ofMonths(7), "2023-06-01T00:00:00", "", "2023-06-01T00:00:00"), // Quarters new PeriodTestCaseData(Period.ofMonths(3), "2023-12-31T23:59:59.99", "", "2023-10-01T00:00:00"), new PeriodTestCaseData(Period.ofMonths(3), "2024-01-01T00:00:00", "", "2024-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofMonths(3), "2024-03-31T23:59:59.99", "", "2024-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofMonths(3), "2024-04-01T00:00:00", "", "2024-04-01T00:00:00"), new PeriodTestCaseData(Period.ofMonths(6), "2023-12-31T23:59:59.99", "", "2023-07-01T00:00:00"), new PeriodTestCaseData(Period.ofMonths(6), "2024-01-01T00:00:00", "", "2024-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofMonths(6), "2024-06-30T23:59:59.99", "", "2024-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofMonths(6), "2024-07-01T00:00:00", "", "2024-07-01T00:00:00"), // Years new PeriodTestCaseData(Period.ofYears(1), "2022-12-31T23:59:59.99", "", "2022-01-01T00:00:00"), new PeriodTestCaseData(Period.ofYears(1), "2023-01-01T00:00:00", "", "2023-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofYears(1), "2023-12-31T23:59:59.99", "", "2023-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofYears(1), "2024-01-01T00:00:00", "", "2024-01-01T00:00:00"), new PeriodTestCaseData(Period.ofYears(5), "2020-12-31T23:59:59.99", "", "2016-01-01T00:00:00"), new PeriodTestCaseData(Period.ofYears(5), "2021-01-01T00:00:00", "", "2021-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofYears(5), "2025-12-31T23:59:59.99", "", "2021-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofYears(5), "2026-01-01T00:00:00", "", "2026-01-01T00:00:00"), // Negative years new PeriodTestCaseData(Period.ofYears(4), "-0004-12-31T23:59:59.99", "", "-0007-01-01T00:00:00"), new PeriodTestCaseData(Period.ofYears(4), "-0003-01-01T00:00:00", "", "-0003-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofYears(4), "-0001-12-31T23:59:59.99", "", "-0003-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofYears(4), "0001-01-01T00:00:00", "", "0001-01-01T00:00:00") ).forEach(c -> TEST_TIMEZONES.forEach(timezone -> { // Convert the timezone to the offset in each local time. // This is required as date strings can't have a zone name as its zone. From b7d63b9b5ee64fe4a45d15d02a1da877dfcf35e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 7 Nov 2025 17:54:19 +0100 Subject: [PATCH 45/54] Added midnight DST tests --- .../function/scalar/date/DateTruncTests.java | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 10e8bb67e1db9..743a70d221dd8 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -112,7 +112,6 @@ public long inputDateAsNanos() { public static List makeTruncDurationTestCases() { List cases = new ArrayList<>(); - String ts = "2023-02-17T10:25:33.38Z"; // Add generic cases for either UTC, fixed timezones and timezones with minutes. // @@ -155,10 +154,10 @@ public static List makeTruncDurationTestCases() { cases.addAll( List.of( // Timezone agnostic (<=1m intervals, "null" for randomized timezones) - new DurationTestCaseData(Duration.ofMillis(100), ts, null, "2023-02-17T10:25:33.30Z"), - new DurationTestCaseData(Duration.ofSeconds(1), ts, null, "2023-02-17T10:25:33Z"), - new DurationTestCaseData(Duration.ofMinutes(1), ts, null, "2023-02-17T10:25:00Z"), - new DurationTestCaseData(Duration.ofSeconds(30), ts, null, "2023-02-17T10:25:30Z"), + new DurationTestCaseData(Duration.ofMillis(100), "2023-02-17T10:25:33.38Z", null, "2023-02-17T10:25:33.30Z"), + new DurationTestCaseData(Duration.ofSeconds(1), "2023-02-17T10:25:33.38Z", null, "2023-02-17T10:25:33Z"), + new DurationTestCaseData(Duration.ofMinutes(1), "2023-02-17T10:25:33.38Z", null, "2023-02-17T10:25:00Z"), + new DurationTestCaseData(Duration.ofSeconds(30), "2023-02-17T10:25:33.38Z", null, "2023-02-17T10:25:30Z"), // Daylight savings boundaries // - +1 -> +2 at 2025-03-30T02:00:00+01:00 @@ -172,6 +171,13 @@ public static List makeTruncDurationTestCases() { new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T02:00:00+01:00", "Europe/Paris", "2025-10-26T00:00:00+02:00"), new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T03:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T04:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), + // Midnight DST (America/Goose_Bay: -3 to -4 at 2010-11-07T00:01:00-03:00) + new DurationTestCaseData(Duration.ofMinutes(1), "2010-11-07T00:00:59-03:00", "America/Goose_Bay", "2010-11-07T00:00:00-03:00"), + new DurationTestCaseData(Duration.ofMinutes(1), "2010-11-07T00:01:00-04:00", "America/Goose_Bay", "2010-11-07T00:01:00-04:00"), + new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-07T00:01:00-04:00", "America/Goose_Bay", "2010-11-07T00:00:00-04:00"), + new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-07T00:02:00-04:00", "America/Goose_Bay", "2010-11-07T00:02:00-04:00"), + new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-06T23:59:59-04:00", "America/Goose_Bay", "2010-11-06T23:58:00-03:00"), + new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-06T20:03:00-03:00", "America/Goose_Bay", "2010-11-06T20:02:00-03:00"), // Bigger intervals new DurationTestCaseData(Duration.ofHours(12), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), new DurationTestCaseData(Duration.ofHours(24), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), @@ -216,7 +222,7 @@ public static List makeTruncPeriodTestCases() { new PeriodTestCaseData(Period.ofYears(5), "2021-01-01T00:00:00", "", "2021-01-01T00:00:00"), // Negative years new PeriodTestCaseData(Period.ofYears(4), "-0004-12-31T23:59:59.99", "", "-0007-01-01T00:00:00"), - new PeriodTestCaseData(Period.ofYears(4), "-0003-01-01T00:00:00", "", "-0003-01-01T00:00:00"), + new PeriodTestCaseData(Period.ofYears(4), "-0003-01-01T00:00:00", "", "-0003-01-01T00:00:00") ).forEach(c -> TEST_TIMEZONES.forEach(timezone -> { // Convert the timezone to the offset in each local time. // This is required as date strings can't have a zone name as its zone. @@ -230,7 +236,11 @@ public static List makeTruncPeriodTestCases() { List.of( // DST boundaries (e.g. New York: -5 to -4 at 2025-03-09T02:00:00-05, and -4 to -5 at 2025-11-02T02:00:00-04) new PeriodTestCaseData(Period.ofDays(1), "2025-03-09T06:00:00-04:00", "America/New_York", "2025-03-09T00:00:00-05:00"), - new PeriodTestCaseData(Period.ofDays(1), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-11-02T00:00:00-04:00") + new PeriodTestCaseData(Period.ofDays(1), "2025-11-02T05:00:00-05:00", "America/New_York", "2025-11-02T00:00:00-04:00"), + // Midnight DST (America/Goose_Bay: -3 to -4 at 2010-11-07T00:01:00-03:00) + new PeriodTestCaseData(Period.ofDays(1), "2010-11-07T00:00:59-03:00", "America/Goose_Bay", "2010-11-07T00:00:00-03:00"), + new PeriodTestCaseData(Period.ofDays(1), "2010-11-07T00:03:00-04:00", "America/Goose_Bay", "2010-11-07T00:00:00-03:00"), + new PeriodTestCaseData(Period.ofDays(1), "2010-11-06T23:01:00-04:00", "America/Goose_Bay", "2010-11-06T00:00:00-03:00") ) ); return cases; From a3e592c195989a6664861b4616983c183106f274 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 7 Nov 2025 17:01:23 +0000 Subject: [PATCH 46/54] [CI] Auto commit changes from spotless --- .../function/scalar/date/DateTruncTests.java | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 743a70d221dd8..0dbd0f9ef9765 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -172,12 +172,42 @@ public static List makeTruncDurationTestCases() { new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T03:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T04:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), // Midnight DST (America/Goose_Bay: -3 to -4 at 2010-11-07T00:01:00-03:00) - new DurationTestCaseData(Duration.ofMinutes(1), "2010-11-07T00:00:59-03:00", "America/Goose_Bay", "2010-11-07T00:00:00-03:00"), - new DurationTestCaseData(Duration.ofMinutes(1), "2010-11-07T00:01:00-04:00", "America/Goose_Bay", "2010-11-07T00:01:00-04:00"), - new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-07T00:01:00-04:00", "America/Goose_Bay", "2010-11-07T00:00:00-04:00"), - new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-07T00:02:00-04:00", "America/Goose_Bay", "2010-11-07T00:02:00-04:00"), - new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-06T23:59:59-04:00", "America/Goose_Bay", "2010-11-06T23:58:00-03:00"), - new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-06T20:03:00-03:00", "America/Goose_Bay", "2010-11-06T20:02:00-03:00"), + new DurationTestCaseData( + Duration.ofMinutes(1), + "2010-11-07T00:00:59-03:00", + "America/Goose_Bay", + "2010-11-07T00:00:00-03:00" + ), + new DurationTestCaseData( + Duration.ofMinutes(1), + "2010-11-07T00:01:00-04:00", + "America/Goose_Bay", + "2010-11-07T00:01:00-04:00" + ), + new DurationTestCaseData( + Duration.ofMinutes(2), + "2010-11-07T00:01:00-04:00", + "America/Goose_Bay", + "2010-11-07T00:00:00-04:00" + ), + new DurationTestCaseData( + Duration.ofMinutes(2), + "2010-11-07T00:02:00-04:00", + "America/Goose_Bay", + "2010-11-07T00:02:00-04:00" + ), + new DurationTestCaseData( + Duration.ofMinutes(2), + "2010-11-06T23:59:59-04:00", + "America/Goose_Bay", + "2010-11-06T23:58:00-03:00" + ), + new DurationTestCaseData( + Duration.ofMinutes(2), + "2010-11-06T20:03:00-03:00", + "America/Goose_Bay", + "2010-11-06T20:02:00-03:00" + ), // Bigger intervals new DurationTestCaseData(Duration.ofHours(12), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), new DurationTestCaseData(Duration.ofHours(24), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), From 3b34ef65e4b5f5aaab96045209182b133beb9794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 7 Nov 2025 18:10:56 +0100 Subject: [PATCH 47/54] Fixed midnight DST test --- .../function/scalar/date/DateTruncTests.java | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 743a70d221dd8..becc186282ce7 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -172,12 +172,42 @@ public static List makeTruncDurationTestCases() { new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T03:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), new DurationTestCaseData(Duration.ofHours(3), "2025-10-26T04:00:00+01:00", "Europe/Paris", "2025-10-26T03:00:00+01:00"), // Midnight DST (America/Goose_Bay: -3 to -4 at 2010-11-07T00:01:00-03:00) - new DurationTestCaseData(Duration.ofMinutes(1), "2010-11-07T00:00:59-03:00", "America/Goose_Bay", "2010-11-07T00:00:00-03:00"), - new DurationTestCaseData(Duration.ofMinutes(1), "2010-11-07T00:01:00-04:00", "America/Goose_Bay", "2010-11-07T00:01:00-04:00"), - new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-07T00:01:00-04:00", "America/Goose_Bay", "2010-11-07T00:00:00-04:00"), - new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-07T00:02:00-04:00", "America/Goose_Bay", "2010-11-07T00:02:00-04:00"), - new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-06T23:59:59-04:00", "America/Goose_Bay", "2010-11-06T23:58:00-03:00"), - new DurationTestCaseData(Duration.ofMinutes(2), "2010-11-06T20:03:00-03:00", "America/Goose_Bay", "2010-11-06T20:02:00-03:00"), + new DurationTestCaseData( + Duration.ofMinutes(1), + "2010-11-07T00:00:59-03:00", + "America/Goose_Bay", + "2010-11-07T00:00:00-03:00" + ), + new DurationTestCaseData( + Duration.ofMinutes(1), + "2010-11-07T00:01:00-04:00", + "America/Goose_Bay", + "2010-11-07T00:01:00-04:00" + ), + new DurationTestCaseData( + Duration.ofMinutes(2), + "2010-11-07T00:01:00-04:00", + "America/Goose_Bay", + "2010-11-07T00:00:00-04:00" + ), + new DurationTestCaseData( + Duration.ofMinutes(2), + "2010-11-07T00:02:00-04:00", + "America/Goose_Bay", + "2010-11-07T00:02:00-04:00" + ), + new DurationTestCaseData( + Duration.ofMinutes(2), + "2010-11-06T23:01:59-04:00", + "America/Goose_Bay", + "2010-11-07T00:00:00-03:00" + ), + new DurationTestCaseData( + Duration.ofMinutes(2), + "2010-11-06T20:03:00-03:00", + "America/Goose_Bay", + "2010-11-06T20:02:00-03:00" + ), // Bigger intervals new DurationTestCaseData(Duration.ofHours(12), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), new DurationTestCaseData(Duration.ofHours(24), "2025-10-26T02:00:00+02:00", "Europe/Rome", "2025-10-26T00:00:00+02:00"), From f9886b19ea99617ab3dd11a1fdba64dddbec7ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Mon, 10 Nov 2025 11:53:01 +0100 Subject: [PATCH 48/54] Simplified timezones for tests --- .../esql/expression/function/scalar/date/DateTruncTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index becc186282ce7..7afcd9969ee01 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -108,7 +108,7 @@ public long inputDateAsNanos() { } } - private static final List TEST_TIMEZONES = List.of("Z", "-08:00", "+14:00", "+11:45", "Europe/Madrid", "America/New_York"); + private static final List TEST_TIMEZONES = List.of("Z", "-08:00", "CET", "America/New_York"); public static List makeTruncDurationTestCases() { List cases = new ArrayList<>(); From df8b7c66499720293ebfede2e2f88af0449d931b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Mon, 10 Nov 2025 14:33:40 +0100 Subject: [PATCH 49/54] Fixed signature generation failing because of nanos assumptions --- .../function/grouping/BucketTests.java | 88 +++++++++++-------- .../function/grouping/TBucketTests.java | 88 +++++++++++-------- .../function/scalar/date/DateTruncTests.java | 88 ++++++++++++------- 3 files changed, 158 insertions(+), 106 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java index 64a5db043b513..5fe0af1acb61b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java @@ -142,22 +142,27 @@ private static void dateCases(List suppliers, String name, Lon } private static void dateTruncCases(List suppliers) { - makeTruncPeriodTestCases().stream() - .map( - data -> List.of( - new TestCaseSupplier( - data.testCaseNameForMillis(), - List.of(DataType.DATETIME, DataType.DATE_PERIOD), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "field"), - new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral() - ), - Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATETIME, - matchesDateMillis(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), + makeTruncPeriodTestCases().stream().map(data -> { + List caseSuppliers = new ArrayList<>(); + + caseSuppliers.add( + new TestCaseSupplier( + data.testCaseNameForMillis(), + List.of(DataType.DATETIME, DataType.DATE_PERIOD), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "field"), + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral() + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + matchesDateMillis(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ); + + if (data.canBeConvertedToNanos()) { + caseSuppliers.add( new TestCaseSupplier( data.testCaseNameForNanos(), List.of(DataType.DATE_NANOS, DataType.DATE_PERIOD), @@ -171,26 +176,33 @@ private static void dateTruncCases(List suppliers) { matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) + ); + } + + return caseSuppliers; + }).forEach(suppliers::addAll); + + makeTruncDurationTestCases().stream().map(data -> { + List caseSuppliers = new ArrayList<>(); + + caseSuppliers.add( + new TestCaseSupplier( + data.testCaseNameForMillis(), + List.of(DataType.DATETIME, DataType.TIME_DURATION), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "field"), + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral() + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + matchesDateMillis(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) - ) - .forEach(suppliers::addAll); + ); - makeTruncDurationTestCases().stream() - .map( - data -> List.of( - new TestCaseSupplier( - data.testCaseNameForMillis(), - List.of(DataType.DATETIME, DataType.TIME_DURATION), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "field"), - new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral() - ), - Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATETIME, - matchesDateMillis(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), + if (data.canBeConvertedToNanos()) { + caseSuppliers.add( new TestCaseSupplier( data.testCaseNameForNanos(), List.of(DataType.DATE_NANOS, DataType.TIME_DURATION), @@ -204,9 +216,11 @@ private static void dateTruncCases(List suppliers) { matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) - ) - ) - .forEach(suppliers::addAll); + ); + } + + return caseSuppliers; + }).forEach(suppliers::addAll); } private static TestCaseSupplier.TypedData dateBound(String name, DataType type, String date) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java index eed495c2fd69f..70f2e0cd51881 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java @@ -127,22 +127,27 @@ private static void dateNanosCasesWithSpan( } private static void dateTruncCases(List suppliers) { - makeTruncPeriodTestCases().stream() - .map( - data -> List.of( - new TestCaseSupplier( - data.testCaseNameForMillis(), - List.of(DataType.DATE_PERIOD, DataType.DATETIME), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "@timestamp") - ), - Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATETIME, - matchesDateMillis(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), + makeTruncPeriodTestCases().stream().map(data -> { + List caseSuppliers = new ArrayList<>(); + + caseSuppliers.add( + new TestCaseSupplier( + data.testCaseNameForMillis(), + List.of(DataType.DATE_PERIOD, DataType.DATETIME), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "@timestamp") + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + matchesDateMillis(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ); + + if (data.canBeConvertedToNanos()) { + caseSuppliers.add( new TestCaseSupplier( data.testCaseNameForNanos(), List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), @@ -156,26 +161,33 @@ private static void dateTruncCases(List suppliers) { matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) + ); + } + + return caseSuppliers; + }).forEach(suppliers::addAll); + + makeTruncDurationTestCases().stream().map(data -> { + List caseSuppliers = new ArrayList<>(); + + caseSuppliers.add( + new TestCaseSupplier( + data.testCaseNameForMillis(), + List.of(DataType.TIME_DURATION, DataType.DATETIME), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "@timestamp") + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + matchesDateMillis(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) - ) - .forEach(suppliers::addAll); + ); - makeTruncDurationTestCases().stream() - .map( - data -> List.of( - new TestCaseSupplier( - data.testCaseNameForMillis(), - List.of(DataType.TIME_DURATION, DataType.DATETIME), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "@timestamp") - ), - Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATETIME, - matchesDateMillis(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), + if (data.canBeConvertedToNanos()) { + caseSuppliers.add( new TestCaseSupplier( data.testCaseNameForNanos(), List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), @@ -189,9 +201,11 @@ private static void dateTruncCases(List suppliers) { matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) - ) - ) - .forEach(suppliers::addAll); + ); + } + + return caseSuppliers; + }).forEach(suppliers::addAll); } private static Matcher resultsMatcher(List typedData) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 7afcd9969ee01..51b2d8757f421 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -76,10 +76,13 @@ public long inputDateAsMillis() { } public long inputDateAsNanos() { - long millis = inputDateAsMillis(); - assumeTrue("Before epoch dates can't be converted to nanos", millis >= 0); + assert canBeConvertedToNanos(); return DateUtils.toNanoSeconds(inputDateAsMillis()); } + + public boolean canBeConvertedToNanos() { + return inputDateAsMillis() >= 0; + } } public record PeriodTestCaseData(Period period, String inputDate, @Nullable String zoneIdString, String expectedDate) { @@ -102,10 +105,13 @@ public long inputDateAsMillis() { } public long inputDateAsNanos() { - long millis = inputDateAsMillis(); - assumeTrue("Before epoch dates can't be converted to nanos", millis >= 0); + assert canBeConvertedToNanos(); return DateUtils.toNanoSeconds(inputDateAsMillis()); } + + public boolean canBeConvertedToNanos() { + return inputDateAsMillis() >= 0; + } } private static final List TEST_TIMEZONES = List.of("Z", "-08:00", "CET", "America/New_York"); @@ -283,7 +289,9 @@ private static String timezoneToOffset(String timezone, String date) { } private static List ofDuration(DurationTestCaseData data) { - return List.of( + List suppliers = new ArrayList<>(); + + suppliers.add( new TestCaseSupplier( data.testCaseNameForMillis(), List.of(DataType.TIME_DURATION, DataType.DATETIME), @@ -296,25 +304,34 @@ private static List ofDuration(DurationTestCaseData data) { DataType.DATETIME, matchesDateMillis(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), - new TestCaseSupplier( - data.testCaseNameForNanos(), - List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") - ), - Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATE_NANOS, - matchesDateNanos(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); + + if (data.canBeConvertedToNanos()) { + suppliers.add( + new TestCaseSupplier( + data.testCaseNameForNanos(), + List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") + ), + Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATE_NANOS, + matchesDateNanos(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ); + } + + return suppliers; } private static List ofDatePeriod(PeriodTestCaseData data) { - return List.of( + List suppliers = new ArrayList<>(); + + suppliers.add( new TestCaseSupplier( data.testCaseNameForMillis(), List.of(DataType.DATE_PERIOD, DataType.DATETIME), @@ -327,21 +344,28 @@ private static List ofDatePeriod(PeriodTestCaseData data) { DataType.DATETIME, matchesDateMillis(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), - new TestCaseSupplier( - data.testCaseNameForNanos(), - List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") - ), - Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATE_NANOS, - matchesDateNanos(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); + + if (data.canBeConvertedToNanos()) { + suppliers.add( + new TestCaseSupplier( + data.testCaseNameForNanos(), + List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") + ), + Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATE_NANOS, + matchesDateNanos(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ); + } + + return suppliers; } private static TestCaseSupplier randomSecond() { From 4723f43398a97f56ce70e1ae2d8302d08dbb4d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Mon, 10 Nov 2025 11:53:01 +0100 Subject: [PATCH 50/54] Simplified timezones for tests --- .../esql/expression/function/scalar/date/DateTruncTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index becc186282ce7..7afcd9969ee01 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -108,7 +108,7 @@ public long inputDateAsNanos() { } } - private static final List TEST_TIMEZONES = List.of("Z", "-08:00", "+14:00", "+11:45", "Europe/Madrid", "America/New_York"); + private static final List TEST_TIMEZONES = List.of("Z", "-08:00", "CET", "America/New_York"); public static List makeTruncDurationTestCases() { List cases = new ArrayList<>(); From 561f99ccf018a11401197f4fcf8c01e94527782b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Mon, 10 Nov 2025 14:33:40 +0100 Subject: [PATCH 51/54] Fixed signature generation failing because of nanos assumptions --- .../function/grouping/BucketTests.java | 88 +++++++++++-------- .../function/grouping/TBucketTests.java | 88 +++++++++++-------- .../function/scalar/date/DateTruncTests.java | 88 ++++++++++++------- 3 files changed, 158 insertions(+), 106 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java index 45db2e9a5792f..091f2f4e102a1 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java @@ -141,22 +141,27 @@ private static void dateCases(List suppliers, String name, Lon } private static void dateTruncCases(List suppliers) { - makeTruncPeriodTestCases().stream() - .map( - data -> List.of( - new TestCaseSupplier( - data.testCaseNameForMillis(), - List.of(DataType.DATETIME, DataType.DATE_PERIOD), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "field"), - new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral() - ), - Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATETIME, - matchesDateMillis(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), + makeTruncPeriodTestCases().stream().map(data -> { + List caseSuppliers = new ArrayList<>(); + + caseSuppliers.add( + new TestCaseSupplier( + data.testCaseNameForMillis(), + List.of(DataType.DATETIME, DataType.DATE_PERIOD), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "field"), + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral() + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + matchesDateMillis(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ); + + if (data.canBeConvertedToNanos()) { + caseSuppliers.add( new TestCaseSupplier( data.testCaseNameForNanos(), List.of(DataType.DATE_NANOS, DataType.DATE_PERIOD), @@ -170,26 +175,33 @@ private static void dateTruncCases(List suppliers) { matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) + ); + } + + return caseSuppliers; + }).forEach(suppliers::addAll); + + makeTruncDurationTestCases().stream().map(data -> { + List caseSuppliers = new ArrayList<>(); + + caseSuppliers.add( + new TestCaseSupplier( + data.testCaseNameForMillis(), + List.of(DataType.DATETIME, DataType.TIME_DURATION), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "field"), + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral() + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + matchesDateMillis(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) - ) - .forEach(suppliers::addAll); + ); - makeTruncDurationTestCases().stream() - .map( - data -> List.of( - new TestCaseSupplier( - data.testCaseNameForMillis(), - List.of(DataType.DATETIME, DataType.TIME_DURATION), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "field"), - new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral() - ), - Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATETIME, - matchesDateMillis(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), + if (data.canBeConvertedToNanos()) { + caseSuppliers.add( new TestCaseSupplier( data.testCaseNameForNanos(), List.of(DataType.DATE_NANOS, DataType.TIME_DURATION), @@ -203,9 +215,11 @@ private static void dateTruncCases(List suppliers) { matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) - ) - ) - .forEach(suppliers::addAll); + ); + } + + return caseSuppliers; + }).forEach(suppliers::addAll); } private static TestCaseSupplier.TypedData dateBound(String name, DataType type, String date) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java index 0d04118771661..939be46c6f45f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/TBucketTests.java @@ -126,22 +126,27 @@ private static void dateNanosCasesWithSpan( } private static void dateTruncCases(List suppliers) { - makeTruncPeriodTestCases().stream() - .map( - data -> List.of( - new TestCaseSupplier( - data.testCaseNameForMillis(), - List.of(DataType.DATE_PERIOD, DataType.DATETIME), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "@timestamp") - ), - Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATETIME, - matchesDateMillis(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), + makeTruncPeriodTestCases().stream().map(data -> { + List caseSuppliers = new ArrayList<>(); + + caseSuppliers.add( + new TestCaseSupplier( + data.testCaseNameForMillis(), + List.of(DataType.DATE_PERIOD, DataType.DATETIME), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "@timestamp") + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + matchesDateMillis(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ); + + if (data.canBeConvertedToNanos()) { + caseSuppliers.add( new TestCaseSupplier( data.testCaseNameForNanos(), List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), @@ -155,26 +160,33 @@ private static void dateTruncCases(List suppliers) { matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) + ); + } + + return caseSuppliers; + }).forEach(suppliers::addAll); + + makeTruncDurationTestCases().stream().map(data -> { + List caseSuppliers = new ArrayList<>(); + + caseSuppliers.add( + new TestCaseSupplier( + data.testCaseNameForMillis(), + List.of(DataType.TIME_DURATION, DataType.DATETIME), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "@timestamp") + ), + Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATETIME, + matchesDateMillis(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) - ) - .forEach(suppliers::addAll); + ); - makeTruncDurationTestCases().stream() - .map( - data -> List.of( - new TestCaseSupplier( - data.testCaseNameForMillis(), - List.of(DataType.TIME_DURATION, DataType.DATETIME), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(data.inputDateAsMillis(), DataType.DATETIME, "@timestamp") - ), - Matchers.startsWith("DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATETIME, - matchesDateMillis(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), + if (data.canBeConvertedToNanos()) { + caseSuppliers.add( new TestCaseSupplier( data.testCaseNameForNanos(), List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), @@ -188,9 +200,11 @@ private static void dateTruncCases(List suppliers) { matchesDateNanos(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) - ) - ) - .forEach(suppliers::addAll); + ); + } + + return caseSuppliers; + }).forEach(suppliers::addAll); } private static Matcher resultsMatcher(List typedData) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java index 7afcd9969ee01..51b2d8757f421 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTruncTests.java @@ -76,10 +76,13 @@ public long inputDateAsMillis() { } public long inputDateAsNanos() { - long millis = inputDateAsMillis(); - assumeTrue("Before epoch dates can't be converted to nanos", millis >= 0); + assert canBeConvertedToNanos(); return DateUtils.toNanoSeconds(inputDateAsMillis()); } + + public boolean canBeConvertedToNanos() { + return inputDateAsMillis() >= 0; + } } public record PeriodTestCaseData(Period period, String inputDate, @Nullable String zoneIdString, String expectedDate) { @@ -102,10 +105,13 @@ public long inputDateAsMillis() { } public long inputDateAsNanos() { - long millis = inputDateAsMillis(); - assumeTrue("Before epoch dates can't be converted to nanos", millis >= 0); + assert canBeConvertedToNanos(); return DateUtils.toNanoSeconds(inputDateAsMillis()); } + + public boolean canBeConvertedToNanos() { + return inputDateAsMillis() >= 0; + } } private static final List TEST_TIMEZONES = List.of("Z", "-08:00", "CET", "America/New_York"); @@ -283,7 +289,9 @@ private static String timezoneToOffset(String timezone, String date) { } private static List ofDuration(DurationTestCaseData data) { - return List.of( + List suppliers = new ArrayList<>(); + + suppliers.add( new TestCaseSupplier( data.testCaseNameForMillis(), List.of(DataType.TIME_DURATION, DataType.DATETIME), @@ -296,25 +304,34 @@ private static List ofDuration(DurationTestCaseData data) { DataType.DATETIME, matchesDateMillis(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), - new TestCaseSupplier( - data.testCaseNameForNanos(), - List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") - ), - Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATE_NANOS, - matchesDateNanos(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); + + if (data.canBeConvertedToNanos()) { + suppliers.add( + new TestCaseSupplier( + data.testCaseNameForNanos(), + List.of(DataType.TIME_DURATION, DataType.DATE_NANOS), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.duration(), DataType.TIME_DURATION, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") + ), + Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATE_NANOS, + matchesDateNanos(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ); + } + + return suppliers; } private static List ofDatePeriod(PeriodTestCaseData data) { - return List.of( + List suppliers = new ArrayList<>(); + + suppliers.add( new TestCaseSupplier( data.testCaseNameForMillis(), List.of(DataType.DATE_PERIOD, DataType.DATETIME), @@ -327,21 +344,28 @@ private static List ofDatePeriod(PeriodTestCaseData data) { DataType.DATETIME, matchesDateMillis(data.expectedDate()) ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) - ), - new TestCaseSupplier( - data.testCaseNameForNanos(), - List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), - () -> new TestCaseSupplier.TestCase( - List.of( - new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), - new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") - ), - Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), - DataType.DATE_NANOS, - matchesDateNanos(data.expectedDate()) - ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) ) ); + + if (data.canBeConvertedToNanos()) { + suppliers.add( + new TestCaseSupplier( + data.testCaseNameForNanos(), + List.of(DataType.DATE_PERIOD, DataType.DATE_NANOS), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(data.period(), DataType.DATE_PERIOD, "interval").forceLiteral(), + new TestCaseSupplier.TypedData(data.inputDateAsNanos(), DataType.DATE_NANOS, "date") + ), + Matchers.startsWith("DateTruncDateNanosEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding["), + DataType.DATE_NANOS, + matchesDateNanos(data.expectedDate()) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(data.zoneId())) + ) + ); + } + + return suppliers; } private static TestCaseSupplier randomSecond() { From 0c91474ca34192b3094590a66baa719db80fff77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 14 Nov 2025 16:24:46 +0100 Subject: [PATCH 52/54] Move string matcher to ReadableMatchers --- .../elasticsearch/test/ReadableMatchers.java | 34 ++++++++++++++++ .../matchers/StringBytesRefMatcher.java | 39 ------------------- .../function/scalar/date/DateFormatTests.java | 6 +-- .../function/scalar/date/DayNameTests.java | 6 +-- .../function/scalar/date/MonthNameTests.java | 6 +-- 5 files changed, 43 insertions(+), 48 deletions(-) delete mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java diff --git a/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java b/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java index b90187a8f04c8..23d7cd012f580 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java @@ -9,6 +9,7 @@ package org.elasticsearch.test; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateUtils; import org.hamcrest.Description; @@ -39,6 +40,13 @@ public static DateNanosMatcher matchesDateNanos(String date) { return new DateNanosMatcher(date); } + /** + * Test matcher for BytesRef that expects BytesRefs, but describes the errors as strings, for better readability. + */ + public static StringBytesRefMatcher matchesBytesRef(String string) { + return new StringBytesRefMatcher(string); + } + public static class DateMillisMatcher extends TypeSafeMatcher { private final long timeMillis; @@ -84,4 +92,30 @@ public void describeTo(Description description) { description.appendText(dateFormatter.formatNanos(timeNanos)); } } + + public static class StringBytesRefMatcher extends TypeSafeMatcher { + private final String string; + private final BytesRef bytesRef; + + public StringBytesRefMatcher(String string) { + this.string = string; + this.bytesRef = new BytesRef(string); + } + + @Override + protected boolean matchesSafely(BytesRef item) { + return item.equals(bytesRef); + } + + @Override + public void describeMismatchSafely(BytesRef item, org.hamcrest.Description description) { + description.appendText("was ").appendValue(item.utf8ToString()); + } + + @Override + public void describeTo(org.hamcrest.Description description) { + description.appendText(string); + } + } + } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java deleted file mode 100644 index 94d484bb17c18..0000000000000 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/common/matchers/StringBytesRefMatcher.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.common.matchers; - -import org.apache.lucene.util.BytesRef; -import org.hamcrest.TypeSafeMatcher; - -/** - * Test matcher for BytesRef that expects BytesRefs, but describes the errors as strings, for better readability. - */ -public class StringBytesRefMatcher extends TypeSafeMatcher { - private final String string; - private final BytesRef bytesRef; - - public StringBytesRefMatcher(String string) { - this.string = string; - this.bytesRef = new BytesRef(string); - } - - @Override - protected boolean matchesSafely(BytesRef item) { - return item.equals(bytesRef); - } - - @Override - public void describeMismatchSafely(BytesRef item, org.hamcrest.Description description) { - description.appendText("was ").appendValue(item.utf8ToString()); - } - - @Override - public void describeTo(org.hamcrest.Description description) { - description.appendText(string); - } -} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java index 0b631c70f33a5..b291c2192538f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateFormatTests.java @@ -13,7 +13,6 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateUtils; -import org.elasticsearch.xpack.esql.common.matchers.StringBytesRefMatcher; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; @@ -28,6 +27,7 @@ import java.util.Locale; import java.util.function.Supplier; +import static org.elasticsearch.test.ReadableMatchers.matchesBytesRef; import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; import static org.hamcrest.Matchers.matchesPattern; @@ -114,7 +114,7 @@ private static List casesFor(String format, String date, Strin + "]" ), DataType.KEYWORD, - new StringBytesRefMatcher(expectedString) + matchesBytesRef(expectedString) ).withConfiguration(TEST_SOURCE, configurationForLocale(locale)) ), new TestCaseSupplier( @@ -131,7 +131,7 @@ private static List casesFor(String format, String date, Strin + "]" ), DataType.KEYWORD, - new StringBytesRefMatcher(expectedString) + matchesBytesRef(expectedString) ).withConfiguration(TEST_SOURCE, configurationForLocale(locale)) ) ); 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 index c36ca1f3e0a59..4fe528a12216c 100644 --- 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 @@ -14,7 +14,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.xpack.esql.analysis.AnalyzerSettings; -import org.elasticsearch.xpack.esql.common.matchers.StringBytesRefMatcher; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.Literal; @@ -35,6 +34,7 @@ import java.util.Map; import java.util.function.Supplier; +import static org.elasticsearch.test.ReadableMatchers.matchesBytesRef; import static org.hamcrest.Matchers.equalTo; public class DayNameTests extends AbstractConfigurationFunctionTestCase { @@ -89,7 +89,7 @@ private static List generateTest(String dateTime, String expec List.of(new TestCaseSupplier.TypedData(toMillis(dateTime), DataType.DATETIME, "date")), Matchers.startsWith("DayNameMillisEvaluator[val=Attribute[channel=0], zoneId=" + zoneId + ", locale=" + locale + "]"), DataType.KEYWORD, - new StringBytesRefMatcher(expectedWeekDay) + matchesBytesRef(expectedWeekDay) ).withConfiguration(TestCaseSupplier.TEST_SOURCE, configurationForTimezoneAndLocale(zoneId, locale)) ), new TestCaseSupplier( @@ -99,7 +99,7 @@ private static List generateTest(String dateTime, String expec List.of(new TestCaseSupplier.TypedData(toNanos(dateTime), DataType.DATE_NANOS, "date")), Matchers.is("DayNameNanosEvaluator[val=Attribute[channel=0], zoneId=" + zoneId + ", locale=" + locale + "]"), DataType.KEYWORD, - new StringBytesRefMatcher(expectedWeekDay) + matchesBytesRef(expectedWeekDay) ).withConfiguration(TestCaseSupplier.TEST_SOURCE, configurationForTimezoneAndLocale(zoneId, locale)) ) ); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java index d308e5663ccb2..698d36232f665 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/MonthNameTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.time.DateUtils; import org.elasticsearch.xpack.esql.analysis.AnalyzerSettings; -import org.elasticsearch.xpack.esql.common.matchers.StringBytesRefMatcher; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.Literal; @@ -35,6 +34,7 @@ import java.util.Map; import java.util.function.Supplier; +import static org.elasticsearch.test.ReadableMatchers.matchesBytesRef; import static org.hamcrest.Matchers.equalTo; public class MonthNameTests extends AbstractConfigurationFunctionTestCase { @@ -97,7 +97,7 @@ private static List generateTest(String dateTime, String expec List.of(new TestCaseSupplier.TypedData(toMillis(dateTime), DataType.DATETIME, "date")), Matchers.startsWith("MonthNameMillisEvaluator[val=Attribute[channel=0], zoneId=" + zoneId + ", locale=" + locale + "]"), DataType.KEYWORD, - new StringBytesRefMatcher(expectedMonthName) + matchesBytesRef(expectedMonthName) ).withConfiguration(TestCaseSupplier.TEST_SOURCE, configurationForTimezoneAndLocale(zoneId, locale)) ), new TestCaseSupplier( @@ -107,7 +107,7 @@ private static List generateTest(String dateTime, String expec List.of(new TestCaseSupplier.TypedData(toNanos(dateTime), DataType.DATE_NANOS, "date")), Matchers.is("MonthNameNanosEvaluator[val=Attribute[channel=0], zoneId=" + zoneId + ", locale=" + locale + "]"), DataType.KEYWORD, - new StringBytesRefMatcher(expectedMonthName) + matchesBytesRef(expectedMonthName) ).withConfiguration(TestCaseSupplier.TEST_SOURCE, configurationForTimezoneAndLocale(zoneId, locale)) ) ); From f162409a016c6935a1c26546478be4d6a018bf50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Fri, 14 Nov 2025 16:34:51 +0100 Subject: [PATCH 53/54] Remove unused method --- .../xpack/esql/expression/function/grouping/Bucket.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java index 6378734ea39cb..1a2261a6f3b21 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java @@ -239,10 +239,6 @@ private Bucket(StreamInput in) throws IOException { ); } - public Configuration configuration() { - return configuration; - } - private static List fields(Expression field, Expression buckets, Expression from, Expression to) { List list = new ArrayList<>(4); list.add(field); From 5b5a33039725cd33e2e00c48d67ac9bdfe05b0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Tue, 18 Nov 2025 13:38:24 +0100 Subject: [PATCH 54/54] Remove redundant qualifier --- .../main/java/org/elasticsearch/test/ReadableMatchers.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java b/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java index 23d7cd012f580..775fce8f5dae8 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ReadableMatchers.java @@ -108,12 +108,12 @@ protected boolean matchesSafely(BytesRef item) { } @Override - public void describeMismatchSafely(BytesRef item, org.hamcrest.Description description) { + public void describeMismatchSafely(BytesRef item, Description description) { description.appendText("was ").appendValue(item.utf8ToString()); } @Override - public void describeTo(org.hamcrest.Description description) { + public void describeTo(Description description) { description.appendText(string); } }