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 cc678b65cf37c..d981f9848ac20 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 @@ -158,7 +158,7 @@ private static EvalOperator.ExpressionEvaluator evaluator(String operation) { FieldAttribute longField = longField(); yield EvalMapper.toEvaluator( FOLD_CONTEXT, - new Add(Source.EMPTY, longField, new Literal(Source.EMPTY, 1L, DataType.LONG)), + new Add(Source.EMPTY, longField, new Literal(Source.EMPTY, 1L, DataType.LONG), configuration()), layout(longField) ).get(driverContext); } @@ -166,7 +166,7 @@ private static EvalOperator.ExpressionEvaluator evaluator(String operation) { FieldAttribute doubleField = doubleField(); yield EvalMapper.toEvaluator( FOLD_CONTEXT, - new Add(Source.EMPTY, doubleField, new Literal(Source.EMPTY, 1D, DataType.DOUBLE)), + new Add(Source.EMPTY, doubleField, new Literal(Source.EMPTY, 1D, DataType.DOUBLE), configuration()), layout(doubleField) ).get(driverContext); } @@ -177,8 +177,8 @@ private static EvalOperator.ExpressionEvaluator evaluator(String operation) { Expression lhs = f1; Expression rhs = f2; if (operation.endsWith("lazy")) { - lhs = new Add(Source.EMPTY, lhs, new Literal(Source.EMPTY, 1L, DataType.LONG)); - rhs = new Add(Source.EMPTY, rhs, new Literal(Source.EMPTY, 1L, DataType.LONG)); + lhs = new Add(Source.EMPTY, lhs, new Literal(Source.EMPTY, 1L, DataType.LONG), configuration()); + rhs = new Add(Source.EMPTY, rhs, new Literal(Source.EMPTY, 1L, DataType.LONG), configuration()); } EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator( FOLD_CONTEXT, @@ -196,7 +196,7 @@ private static EvalOperator.ExpressionEvaluator evaluator(String operation) { FieldAttribute f2 = longField(); Expression lhs = f1; if (operation.endsWith("lazy")) { - lhs = new Add(Source.EMPTY, lhs, new Literal(Source.EMPTY, 1L, DataType.LONG)); + lhs = new Add(Source.EMPTY, lhs, new Literal(Source.EMPTY, 1L, DataType.LONG), configuration()); } EvalOperator.ExpressionEvaluator evaluator = EvalMapper.toEvaluator( FOLD_CONTEXT, diff --git a/docs/changelog/140101.yaml b/docs/changelog/140101.yaml new file mode 100644 index 0000000000000..d4073a2545dd1 --- /dev/null +++ b/docs/changelog/140101.yaml @@ -0,0 +1,6 @@ +pr: 140101 +summary: "Add timezone to add and sub operators, and `ConfigurationAware` planning\ + \ support" +area: ES|QL +type: feature +issues: [] 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 3a7c767961308..e9e33737490ae 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 @@ -1346,6 +1346,21 @@ a:datetime null ; +dateMathWithTimezonesRow +required_capability: add_sub_operator_timezone_support + +set time_zone="America/New_York"\; +row + date_add = "2025-03-09T01:02:03.123-05:00" + 1d, + date_sub = "2025-03-10T01:02:03.123-04:00" - 1d, + nanos_add = "2025-03-09T01:02:03.123000001-05:00"::date_nanos + 1d, + nanos_sub = "2025-03-10T01:02:03.123000001-04:00"::date_nanos - 1d +; + +date_add:datetime | date_sub:datetime | nanos_add:date_nanos | nanos_sub:date_nanos +2025-03-10T05:02:03.123Z | 2025-03-09T06:02:03.123Z | 2025-03-10T05:02:03.123000001Z | 2025-03-09T06:02:03.123000001Z +; + ImplicitCastingEqual required_capability: rangequery_for_datetime from employees diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/row.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/row.csv-spec index 17ca3ff072ea6..c925f00a56a56 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/row.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/row.csv-spec @@ -76,6 +76,20 @@ a:integer | b:integer | c:integer 1 | 2 | 3 ; +rowWithArithmeticOperators +row a = 1 + 4; + +a:integer +5 +; + +rowWithComparisonOperators +row a = 1 < 4; + +a:boolean +true +; + evalRowWithNull row a = 1, b = 2, c = null | eval z = c+b+a; diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddDateNanosEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddDateNanosEvaluator.java index 648d966264fdc..871eff2328ee3 100644 --- a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddDateNanosEvaluator.java +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddDateNanosEvaluator.java @@ -9,6 +9,7 @@ import java.lang.Override; import java.lang.String; import java.time.DateTimeException; +import java.time.ZoneId; import java.time.temporal.TemporalAmount; import org.apache.lucene.util.RamUsageEstimator; import org.elasticsearch.compute.data.Block; @@ -34,15 +35,18 @@ public final class AddDateNanosEvaluator implements EvalOperator.ExpressionEvalu private final TemporalAmount temporalAmount; + private final ZoneId zoneId; + private final DriverContext driverContext; private Warnings warnings; public AddDateNanosEvaluator(Source source, EvalOperator.ExpressionEvaluator dateNanos, - TemporalAmount temporalAmount, DriverContext driverContext) { + TemporalAmount temporalAmount, ZoneId zoneId, DriverContext driverContext) { this.source = source; this.dateNanos = dateNanos; this.temporalAmount = temporalAmount; + this.zoneId = zoneId; this.driverContext = driverContext; } @@ -80,7 +84,7 @@ public LongBlock eval(int positionCount, LongBlock dateNanosBlock) { } long dateNanos = dateNanosBlock.getLong(dateNanosBlock.getFirstValueIndex(p)); try { - result.appendLong(Add.processDateNanos(dateNanos, this.temporalAmount)); + result.appendLong(Add.processDateNanos(dateNanos, this.temporalAmount, this.zoneId)); } catch (ArithmeticException | DateTimeException e) { warnings().registerException(e); result.appendNull(); @@ -95,7 +99,7 @@ public LongBlock eval(int positionCount, LongVector dateNanosVector) { position: for (int p = 0; p < positionCount; p++) { long dateNanos = dateNanosVector.getLong(p); try { - result.appendLong(Add.processDateNanos(dateNanos, this.temporalAmount)); + result.appendLong(Add.processDateNanos(dateNanos, this.temporalAmount, this.zoneId)); } catch (ArithmeticException | DateTimeException e) { warnings().registerException(e); result.appendNull(); @@ -107,7 +111,7 @@ public LongBlock eval(int positionCount, LongVector dateNanosVector) { @Override public String toString() { - return "AddDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + "]"; + return "AddDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + ", zoneId=" + zoneId + "]"; } @Override @@ -134,21 +138,24 @@ static class Factory implements EvalOperator.ExpressionEvaluator.Factory { private final TemporalAmount temporalAmount; + private final ZoneId zoneId; + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory dateNanos, - TemporalAmount temporalAmount) { + TemporalAmount temporalAmount, ZoneId zoneId) { this.source = source; this.dateNanos = dateNanos; this.temporalAmount = temporalAmount; + this.zoneId = zoneId; } @Override public AddDateNanosEvaluator get(DriverContext context) { - return new AddDateNanosEvaluator(source, dateNanos.get(context), temporalAmount, context); + return new AddDateNanosEvaluator(source, dateNanos.get(context), temporalAmount, zoneId, context); } @Override public String toString() { - return "AddDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + "]"; + return "AddDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + ", zoneId=" + zoneId + "]"; } } } diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddDatetimesEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddDatetimesEvaluator.java index ebfc5e29dbef4..babe23a128fdd 100644 --- a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddDatetimesEvaluator.java +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddDatetimesEvaluator.java @@ -9,6 +9,7 @@ import java.lang.Override; import java.lang.String; import java.time.DateTimeException; +import java.time.ZoneId; import java.time.temporal.TemporalAmount; import org.apache.lucene.util.RamUsageEstimator; import org.elasticsearch.compute.data.Block; @@ -34,15 +35,18 @@ public final class AddDatetimesEvaluator implements EvalOperator.ExpressionEvalu private final TemporalAmount temporalAmount; + private final ZoneId zoneId; + private final DriverContext driverContext; private Warnings warnings; public AddDatetimesEvaluator(Source source, EvalOperator.ExpressionEvaluator datetime, - TemporalAmount temporalAmount, DriverContext driverContext) { + TemporalAmount temporalAmount, ZoneId zoneId, DriverContext driverContext) { this.source = source; this.datetime = datetime; this.temporalAmount = temporalAmount; + this.zoneId = zoneId; this.driverContext = driverContext; } @@ -80,7 +84,7 @@ public LongBlock eval(int positionCount, LongBlock datetimeBlock) { } long datetime = datetimeBlock.getLong(datetimeBlock.getFirstValueIndex(p)); try { - result.appendLong(Add.processDatetimes(datetime, this.temporalAmount)); + result.appendLong(Add.processDatetimes(datetime, this.temporalAmount, this.zoneId)); } catch (ArithmeticException | DateTimeException e) { warnings().registerException(e); result.appendNull(); @@ -95,7 +99,7 @@ public LongBlock eval(int positionCount, LongVector datetimeVector) { position: for (int p = 0; p < positionCount; p++) { long datetime = datetimeVector.getLong(p); try { - result.appendLong(Add.processDatetimes(datetime, this.temporalAmount)); + result.appendLong(Add.processDatetimes(datetime, this.temporalAmount, this.zoneId)); } catch (ArithmeticException | DateTimeException e) { warnings().registerException(e); result.appendNull(); @@ -107,7 +111,7 @@ public LongBlock eval(int positionCount, LongVector datetimeVector) { @Override public String toString() { - return "AddDatetimesEvaluator[" + "datetime=" + datetime + ", temporalAmount=" + temporalAmount + "]"; + return "AddDatetimesEvaluator[" + "datetime=" + datetime + ", temporalAmount=" + temporalAmount + ", zoneId=" + zoneId + "]"; } @Override @@ -134,21 +138,24 @@ static class Factory implements EvalOperator.ExpressionEvaluator.Factory { private final TemporalAmount temporalAmount; + private final ZoneId zoneId; + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory datetime, - TemporalAmount temporalAmount) { + TemporalAmount temporalAmount, ZoneId zoneId) { this.source = source; this.datetime = datetime; this.temporalAmount = temporalAmount; + this.zoneId = zoneId; } @Override public AddDatetimesEvaluator get(DriverContext context) { - return new AddDatetimesEvaluator(source, datetime.get(context), temporalAmount, context); + return new AddDatetimesEvaluator(source, datetime.get(context), temporalAmount, zoneId, context); } @Override public String toString() { - return "AddDatetimesEvaluator[" + "datetime=" + datetime + ", temporalAmount=" + temporalAmount + "]"; + return "AddDatetimesEvaluator[" + "datetime=" + datetime + ", temporalAmount=" + temporalAmount + ", zoneId=" + zoneId + "]"; } } } diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubDateNanosEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubDateNanosEvaluator.java index de434d255dba4..f287cc0ca54be 100644 --- a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubDateNanosEvaluator.java +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubDateNanosEvaluator.java @@ -9,6 +9,7 @@ import java.lang.Override; import java.lang.String; import java.time.DateTimeException; +import java.time.ZoneId; import java.time.temporal.TemporalAmount; import org.apache.lucene.util.RamUsageEstimator; import org.elasticsearch.compute.data.Block; @@ -34,15 +35,18 @@ public final class SubDateNanosEvaluator implements EvalOperator.ExpressionEvalu private final TemporalAmount temporalAmount; + private final ZoneId zoneId; + private final DriverContext driverContext; private Warnings warnings; public SubDateNanosEvaluator(Source source, EvalOperator.ExpressionEvaluator dateNanos, - TemporalAmount temporalAmount, DriverContext driverContext) { + TemporalAmount temporalAmount, ZoneId zoneId, DriverContext driverContext) { this.source = source; this.dateNanos = dateNanos; this.temporalAmount = temporalAmount; + this.zoneId = zoneId; this.driverContext = driverContext; } @@ -80,7 +84,7 @@ public LongBlock eval(int positionCount, LongBlock dateNanosBlock) { } long dateNanos = dateNanosBlock.getLong(dateNanosBlock.getFirstValueIndex(p)); try { - result.appendLong(Sub.processDateNanos(dateNanos, this.temporalAmount)); + result.appendLong(Sub.processDateNanos(dateNanos, this.temporalAmount, this.zoneId)); } catch (ArithmeticException | DateTimeException e) { warnings().registerException(e); result.appendNull(); @@ -95,7 +99,7 @@ public LongBlock eval(int positionCount, LongVector dateNanosVector) { position: for (int p = 0; p < positionCount; p++) { long dateNanos = dateNanosVector.getLong(p); try { - result.appendLong(Sub.processDateNanos(dateNanos, this.temporalAmount)); + result.appendLong(Sub.processDateNanos(dateNanos, this.temporalAmount, this.zoneId)); } catch (ArithmeticException | DateTimeException e) { warnings().registerException(e); result.appendNull(); @@ -107,7 +111,7 @@ public LongBlock eval(int positionCount, LongVector dateNanosVector) { @Override public String toString() { - return "SubDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + "]"; + return "SubDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + ", zoneId=" + zoneId + "]"; } @Override @@ -134,21 +138,24 @@ static class Factory implements EvalOperator.ExpressionEvaluator.Factory { private final TemporalAmount temporalAmount; + private final ZoneId zoneId; + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory dateNanos, - TemporalAmount temporalAmount) { + TemporalAmount temporalAmount, ZoneId zoneId) { this.source = source; this.dateNanos = dateNanos; this.temporalAmount = temporalAmount; + this.zoneId = zoneId; } @Override public SubDateNanosEvaluator get(DriverContext context) { - return new SubDateNanosEvaluator(source, dateNanos.get(context), temporalAmount, context); + return new SubDateNanosEvaluator(source, dateNanos.get(context), temporalAmount, zoneId, context); } @Override public String toString() { - return "SubDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + "]"; + return "SubDateNanosEvaluator[" + "dateNanos=" + dateNanos + ", temporalAmount=" + temporalAmount + ", zoneId=" + zoneId + "]"; } } } diff --git a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubDatetimesEvaluator.java b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubDatetimesEvaluator.java index e0f3cfeb4b187..6454c332c025b 100644 --- a/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubDatetimesEvaluator.java +++ b/x-pack/plugin/esql/src/main/generated/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubDatetimesEvaluator.java @@ -9,6 +9,7 @@ import java.lang.Override; import java.lang.String; import java.time.DateTimeException; +import java.time.ZoneId; import java.time.temporal.TemporalAmount; import org.apache.lucene.util.RamUsageEstimator; import org.elasticsearch.compute.data.Block; @@ -34,15 +35,18 @@ public final class SubDatetimesEvaluator implements EvalOperator.ExpressionEvalu private final TemporalAmount temporalAmount; + private final ZoneId zoneId; + private final DriverContext driverContext; private Warnings warnings; public SubDatetimesEvaluator(Source source, EvalOperator.ExpressionEvaluator datetime, - TemporalAmount temporalAmount, DriverContext driverContext) { + TemporalAmount temporalAmount, ZoneId zoneId, DriverContext driverContext) { this.source = source; this.datetime = datetime; this.temporalAmount = temporalAmount; + this.zoneId = zoneId; this.driverContext = driverContext; } @@ -80,7 +84,7 @@ public LongBlock eval(int positionCount, LongBlock datetimeBlock) { } long datetime = datetimeBlock.getLong(datetimeBlock.getFirstValueIndex(p)); try { - result.appendLong(Sub.processDatetimes(datetime, this.temporalAmount)); + result.appendLong(Sub.processDatetimes(datetime, this.temporalAmount, this.zoneId)); } catch (ArithmeticException | DateTimeException e) { warnings().registerException(e); result.appendNull(); @@ -95,7 +99,7 @@ public LongBlock eval(int positionCount, LongVector datetimeVector) { position: for (int p = 0; p < positionCount; p++) { long datetime = datetimeVector.getLong(p); try { - result.appendLong(Sub.processDatetimes(datetime, this.temporalAmount)); + result.appendLong(Sub.processDatetimes(datetime, this.temporalAmount, this.zoneId)); } catch (ArithmeticException | DateTimeException e) { warnings().registerException(e); result.appendNull(); @@ -107,7 +111,7 @@ public LongBlock eval(int positionCount, LongVector datetimeVector) { @Override public String toString() { - return "SubDatetimesEvaluator[" + "datetime=" + datetime + ", temporalAmount=" + temporalAmount + "]"; + return "SubDatetimesEvaluator[" + "datetime=" + datetime + ", temporalAmount=" + temporalAmount + ", zoneId=" + zoneId + "]"; } @Override @@ -134,21 +138,24 @@ static class Factory implements EvalOperator.ExpressionEvaluator.Factory { private final TemporalAmount temporalAmount; + private final ZoneId zoneId; + public Factory(Source source, EvalOperator.ExpressionEvaluator.Factory datetime, - TemporalAmount temporalAmount) { + TemporalAmount temporalAmount, ZoneId zoneId) { this.source = source; this.datetime = datetime; this.temporalAmount = temporalAmount; + this.zoneId = zoneId; } @Override public SubDatetimesEvaluator get(DriverContext context) { - return new SubDatetimesEvaluator(source, datetime.get(context), temporalAmount, context); + return new SubDatetimesEvaluator(source, datetime.get(context), temporalAmount, zoneId, context); } @Override public String toString() { - return "SubDatetimesEvaluator[" + "datetime=" + datetime + ", temporalAmount=" + temporalAmount + "]"; + return "SubDatetimesEvaluator[" + "datetime=" + datetime + ", temporalAmount=" + temporalAmount + ", zoneId=" + zoneId + "]"; } } } 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 b80f883c933a4..8a1a7f1cad334 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 @@ -1351,6 +1351,11 @@ public enum Cap { */ DATE_FORMAT_DATE_PARSE_TIMEZONE_SUPPORT(Build.current().isSnapshot()), + /** + * Support timezones in + and - operators. + */ + ADD_SUB_OPERATOR_TIMEZONE_SUPPORT(Build.current().isSnapshot()), + /** * (Re)Added EXPLAIN command */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java index 09b72027239b4..706a38f61225c 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java @@ -22,6 +22,7 @@ import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException; import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.analysis.AnalyzerRules.ParameterizedAnalyzerRule; +import org.elasticsearch.xpack.esql.capabilities.ConfigurationAware; import org.elasticsearch.xpack.esql.capabilities.TranslationAware; import org.elasticsearch.xpack.esql.common.Failure; import org.elasticsearch.xpack.esql.core.capabilities.Resolvables; @@ -214,6 +215,7 @@ public class Analyzer extends ParameterizedRuleExecutor( "Initialize", Limiter.ONCE, + new ResolveConfigurationAware(), new ResolveTable(), new PruneEmptyUnionAllBranch(), new ResolveEnrich(), @@ -1451,6 +1453,29 @@ private static Attribute handleSpecialFields(UnresolvedAttribute u, Attribute na return named.withLocation(u.source()); } + private static class ResolveConfigurationAware extends ParameterizedAnalyzerRule { + + @Override + protected boolean skipResolved() { + return false; + } + + @Override + protected LogicalPlan rule(LogicalPlan plan, AnalyzerContext context) { + return plan.transformExpressionsUp( + Expression.class, + expression -> resolveConfigurationAware(expression, context.configuration()) + ); + } + + private static Expression resolveConfigurationAware(Expression expression, Configuration configuration) { + if (expression instanceof ConfigurationAware ca && ca.configuration() == ConfigurationAware.CONFIGURATION_MARKER) { + return ca.withConfiguration(configuration); + } + return expression; + } + } + private static class ResolveFunctions extends ParameterizedAnalyzerRule { @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java index 145665d251cf9..2816b3fe89cc3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java @@ -9,6 +9,7 @@ import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.xpack.esql.LicenseAware; +import org.elasticsearch.xpack.esql.capabilities.ConfigurationAware; import org.elasticsearch.xpack.esql.capabilities.PostAnalysisPlanVerificationAware; import org.elasticsearch.xpack.esql.capabilities.PostAnalysisVerificationAware; import org.elasticsearch.xpack.esql.common.Failure; @@ -91,6 +92,8 @@ Collection verify(LogicalPlan plan, BitSet partialMetrics) { // quick verification for unresolved attributes checkUnresolvedAttributes(plan, failures); + ConfigurationAware.verifyNoMarkerConfiguration(plan, failures); + // in case of failures bail-out as all other checks will be redundant if (failures.hasFailures()) { return failures.failures(); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/capabilities/ConfigurationAware.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/capabilities/ConfigurationAware.java index c6c8b19383cc3..21123eee83fc3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/capabilities/ConfigurationAware.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/capabilities/ConfigurationAware.java @@ -7,7 +7,11 @@ package org.elasticsearch.xpack.esql.capabilities; +import org.elasticsearch.xpack.esql.common.Failures; +import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.util.StringUtils; +import org.elasticsearch.xpack.esql.expression.function.ConfigurationFunction; +import org.elasticsearch.xpack.esql.plan.QueryPlan; import org.elasticsearch.xpack.esql.plugin.QueryPragmas; import org.elasticsearch.xpack.esql.session.Configuration; @@ -15,8 +19,19 @@ import java.util.Locale; import java.util.Map; -// See https://github.com/elastic/elasticsearch/issues/138203 -public interface ConfigurationAware { +import static org.elasticsearch.xpack.esql.common.Failure.fail; + +/** + * Interface for plan nodes that require the Configuration at parsing time. + *

+ * They should be created with {@link ConfigurationAware#CONFIGURATION_MARKER}, + * and it will be resolved in the {@link org.elasticsearch.xpack.esql.analysis.Analyzer}. + *

+ *

+ * See https://github.com/elastic/elasticsearch/issues/138203 + *

+ */ +public interface ConfigurationAware extends ConfigurationFunction { // Configuration placeholder used by the Analyzer to replace Configuration CONFIGURATION_MARKER = new Configuration( @@ -39,5 +54,19 @@ public interface ConfigurationAware { Configuration configuration(); - T withConfiguration(Configuration configuration); + Expression withConfiguration(Configuration configuration); + + /** + * Used in the verifiers to ensure the marker configuration is not present in the plan. + *

+ * This should never happen, and a failure here means that we're injecting the marker after the Analyzer, where it's being replaced. + *

+ */ + static void verifyNoMarkerConfiguration(QueryPlan plan, Failures failures) { + plan.forEachExpressionDown(Expression.class, e -> { + if (e instanceof ConfigurationAware ca && ca.configuration() == ConfigurationAware.CONFIGURATION_MARKER) { + failures.add(fail(plan, "Configuration marker found in node {} of plan: {}", e.nodeString(), plan)); + } + }); + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/core/type/DataTypeConverter.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/core/type/DataTypeConverter.java index 7c91a506697c1..0aee65fc7cf2c 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/core/type/DataTypeConverter.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/core/type/DataTypeConverter.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; import java.util.Locale; @@ -518,7 +519,7 @@ private static Function fromDateTime(Function conv } private static Function toDateTime(Converter conversion) { - return l -> DateUtils.asDateTime(((Number) conversion.convert(l)).longValue()); + return l -> DateUtils.asDateTime(((Number) conversion.convert(l)).longValue(), ZoneOffset.UTC); } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/core/util/DateUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/core/util/DateUtils.java index ae316460dd11c..ed3b26b91b773 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/core/util/DateUtils.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/core/util/DateUtils.java @@ -170,12 +170,12 @@ private static String indent(long timeUnit) { /** * Creates a datetime from the millis since epoch (thus the time-zone is UTC). */ - public static ZonedDateTime asDateTime(long millis) { - return ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), UTC); + public static ZonedDateTime asDateTime(long millis, ZoneId zoneId) { + return asDateTime(Instant.ofEpochMilli(millis), zoneId); } - public static ZonedDateTime asDateTime(Instant instant) { - return ZonedDateTime.ofInstant(instant, UTC); + public static ZonedDateTime asDateTime(Instant instant, ZoneId zoneId) { + return ZonedDateTime.ofInstant(instant, zoneId); } public static long asMillis(ZonedDateTime zonedDateTime) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Add.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Add.java index ebfb0fdee47fa..fa4c1402ca685 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Add.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Add.java @@ -18,13 +18,15 @@ import org.elasticsearch.xpack.esql.core.util.NumericUtils; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; +import org.elasticsearch.xpack.esql.session.Configuration; import java.io.IOException; import java.time.DateTimeException; import java.time.Duration; import java.time.Instant; import java.time.Period; -import java.time.ZonedDateTime; +import java.time.ZoneId; import java.time.temporal.TemporalAmount; import static org.elasticsearch.xpack.esql.core.util.DateUtils.asDateTime; @@ -35,10 +37,12 @@ public class Add extends DateTimeArithmeticOperation implements BinaryComparisonInversible { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Add", Add::new); + private final Configuration configuration; + @FunctionInfo( operator = "+", returnType = { "double", "integer", "long", "date_nanos", "date_period", "datetime", "time_duration", "unsigned_long" }, - description = "Add two numbers together. " + "If either field is <> then the result is `null`." + description = "Add two numbers together. If either field is <> then the result is `null`." ) public Add( Source source, @@ -51,7 +55,8 @@ public Add( name = "rhs", description = "A numeric value or a date time value.", type = { "double", "integer", "long", "date_nanos", "date_period", "datetime", "time_duration", "unsigned_long" } - ) Expression right + ) Expression right, + Configuration configuration ) { super( source, @@ -65,6 +70,7 @@ public Add( AddDatetimesEvaluator.Factory::new, AddDateNanosEvaluator.Factory::new ); + this.configuration = configuration; } private Add(StreamInput in) throws IOException { @@ -78,6 +84,7 @@ private Add(StreamInput in) throws IOException { AddDatetimesEvaluator.Factory::new, AddDateNanosEvaluator.Factory::new ); + this.configuration = ((PlanStreamInput) in).configuration(); } @Override @@ -87,22 +94,22 @@ public String getWriteableName() { @Override protected NodeInfo info() { - return NodeInfo.create(this, Add::new, left(), right()); + return NodeInfo.create(this, Add::new, left(), right(), configuration); } @Override protected Add replaceChildren(Expression left, Expression right) { - return new Add(source(), left, right); + return new Add(source(), left, right, configuration); } @Override public Add swapLeftAndRight() { - return new Add(source(), right(), left()); + return new Add(source(), right(), left(), configuration); } @Override public ArithmeticOperationFactory binaryComparisonInverse() { - return Sub::new; + return (source, left, right) -> new Sub(source, left, right, configuration); } @Override @@ -131,21 +138,16 @@ static double processDoubles(double lhs, double rhs) { } @Evaluator(extraName = "Datetimes", warnExceptions = { ArithmeticException.class, DateTimeException.class }) - static long processDatetimes(long datetime, @Fixed TemporalAmount temporalAmount) { + static long processDatetimes(long datetime, @Fixed TemporalAmount temporalAmount, @Fixed ZoneId zoneId) { // using a UTC conversion since `datetime` is always a UTC-Epoch timestamp, either read from ES or converted through a function - return asMillis(asDateTime(datetime).plus(temporalAmount)); + return asMillis(asDateTime(datetime, zoneId).plus(temporalAmount)); } @Evaluator(extraName = "DateNanos", warnExceptions = { ArithmeticException.class, DateTimeException.class }) - static long processDateNanos(long dateNanos, @Fixed TemporalAmount temporalAmount) { + static long processDateNanos(long dateNanos, @Fixed TemporalAmount temporalAmount, @Fixed ZoneId zoneId) { // Instant.plus behaves differently from ZonedDateTime.plus, but DateUtils generally works with instants. try { - return DateUtils.toLong( - Instant.from( - ZonedDateTime.ofInstant(DateUtils.toInstant(dateNanos), org.elasticsearch.xpack.esql.core.util.DateUtils.UTC) - .plus(temporalAmount) - ) - ); + return DateUtils.toLong(Instant.from(asDateTime(DateUtils.toInstant(dateNanos), zoneId).plus(temporalAmount))); } catch (IllegalArgumentException e) { /* toLong will throw IllegalArgumentException for out of range dates, but that includes the actual value which we want @@ -164,4 +166,14 @@ public Period fold(Period left, Period right) { public Duration fold(Duration left, Duration right) { return left.plus(right); } + + @Override + public Configuration configuration() { + return configuration; + } + + @Override + public Add withConfiguration(Configuration configuration) { + return new Add(source(), left(), right(), configuration); + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java index 424c080c905e3..de402074e83b2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/DateTimeArithmeticOperation.java @@ -10,6 +10,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator; import org.elasticsearch.xpack.esql.ExceptionUtils; +import org.elasticsearch.xpack.esql.capabilities.ConfigurationAware; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; @@ -19,8 +20,10 @@ import java.io.IOException; import java.time.Duration; import java.time.Period; +import java.time.ZoneId; import java.time.temporal.TemporalAmount; import java.util.Collection; +import java.util.Objects; import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME; import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS; @@ -31,10 +34,15 @@ import static org.elasticsearch.xpack.esql.core.type.DataType.isNull; import static org.elasticsearch.xpack.esql.core.type.DataType.isTemporalAmount; -public abstract class DateTimeArithmeticOperation extends EsqlArithmeticOperation { +public abstract class DateTimeArithmeticOperation extends EsqlArithmeticOperation implements ConfigurationAware { /** Arithmetic (quad) function. */ interface DatetimeArithmeticEvaluator { - ExpressionEvaluator.Factory apply(Source source, ExpressionEvaluator.Factory expressionEvaluator, TemporalAmount temporalAmount); + ExpressionEvaluator.Factory apply( + Source source, + ExpressionEvaluator.Factory expressionEvaluator, + TemporalAmount temporalAmount, + ZoneId zoneId + ); } private final DatetimeArithmeticEvaluator millisEvaluator; @@ -167,6 +175,7 @@ public final Object fold(FoldContext ctx) { @Override public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { + ZoneId zoneId = configuration().zoneId(); if (dataType() == DATETIME) { // One of the arguments has to be a datetime and the other a temporal amount. Expression datetimeArgument; @@ -182,7 +191,8 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { return millisEvaluator.apply( source(), toEvaluator.apply(datetimeArgument), - (TemporalAmount) temporalAmountArgument.fold(toEvaluator.foldCtx()) + (TemporalAmount) temporalAmountArgument.fold(toEvaluator.foldCtx()), + zoneId ); } else if (dataType() == DATE_NANOS) { // One of the arguments has to be a date_nanos and the other a temporal amount. @@ -199,10 +209,26 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { return nanosEvaluator.apply( source(), toEvaluator.apply(dateNanosArgument), - (TemporalAmount) temporalAmountArgument.fold(toEvaluator.foldCtx()) + (TemporalAmount) temporalAmountArgument.fold(toEvaluator.foldCtx()), + zoneId ); } else { return super.toEvaluator(toEvaluator); } } + + @Override + public int hashCode() { + return Objects.hash(getClass(), children(), configuration()); + } + + @Override + public boolean equals(Object obj) { + if (super.equals(obj) == false) { + return false; + } + DateTimeArithmeticOperation other = (DateTimeArithmeticOperation) obj; + + return configuration().equals(other.configuration()); + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Sub.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Sub.java index 90d7e34a43a8a..48b5e3c4c12a0 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Sub.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/Sub.java @@ -19,13 +19,15 @@ import org.elasticsearch.xpack.esql.core.util.NumericUtils; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; +import org.elasticsearch.xpack.esql.session.Configuration; import java.io.IOException; import java.time.DateTimeException; import java.time.Duration; import java.time.Instant; import java.time.Period; -import java.time.ZonedDateTime; +import java.time.ZoneId; import java.time.temporal.TemporalAmount; import static org.elasticsearch.common.logging.LoggerMessageFormat.format; @@ -37,6 +39,8 @@ public class Sub extends DateTimeArithmeticOperation implements BinaryComparisonInversible { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Sub", Sub::new); + private final Configuration configuration; + @FunctionInfo( operator = "-", returnType = { "double", "integer", "long", "date_period", "datetime", "time_duration", "unsigned_long" }, @@ -54,7 +58,8 @@ public Sub( name = "rhs", description = "A numeric value or a date time value.", type = { "double", "integer", "long", "date_period", "datetime", "time_duration", "unsigned_long" } - ) Expression right + ) Expression right, + Configuration configuration ) { super( source, @@ -68,6 +73,7 @@ public Sub( SubDatetimesEvaluator.Factory::new, SubDateNanosEvaluator.Factory::new ); + this.configuration = configuration; } private Sub(StreamInput in) throws IOException { @@ -81,6 +87,7 @@ private Sub(StreamInput in) throws IOException { SubDatetimesEvaluator.Factory::new, SubDateNanosEvaluator.Factory::new ); + this.configuration = ((PlanStreamInput) in).configuration(); } @Override @@ -110,17 +117,17 @@ protected TypeResolution resolveType() { @Override protected NodeInfo info() { - return NodeInfo.create(this, Sub::new, left(), right()); + return NodeInfo.create(this, Sub::new, left(), right(), configuration); } @Override protected Sub replaceChildren(Expression left, Expression right) { - return new Sub(source(), left, right); + return new Sub(source(), left, right, configuration); } @Override public ArithmeticOperationFactory binaryComparisonInverse() { - return Add::new; + return (source, left, right) -> new Add(source, left, right, configuration); } @Evaluator(extraName = "Ints", warnExceptions = { ArithmeticException.class }) @@ -144,21 +151,16 @@ static double processDoubles(double lhs, double rhs) { } @Evaluator(extraName = "Datetimes", warnExceptions = { ArithmeticException.class, DateTimeException.class }) - static long processDatetimes(long datetime, @Fixed TemporalAmount temporalAmount) { + static long processDatetimes(long datetime, @Fixed TemporalAmount temporalAmount, @Fixed ZoneId zoneId) { // using a UTC conversion since `datetime` is always a UTC-Epoch timestamp, either read from ES or converted through a function - return asMillis(asDateTime(datetime).minus(temporalAmount)); + return asMillis(asDateTime(datetime, zoneId).minus(temporalAmount)); } @Evaluator(extraName = "DateNanos", warnExceptions = { ArithmeticException.class, DateTimeException.class }) - static long processDateNanos(long dateNanos, @Fixed TemporalAmount temporalAmount) { + static long processDateNanos(long dateNanos, @Fixed TemporalAmount temporalAmount, @Fixed ZoneId zoneId) { // Instant.plus behaves differently from ZonedDateTime.plus, but DateUtils generally works with instants. try { - return DateUtils.toLong( - Instant.from( - ZonedDateTime.ofInstant(DateUtils.toInstant(dateNanos), org.elasticsearch.xpack.esql.core.util.DateUtils.UTC) - .minus(temporalAmount) - ) - ); + return DateUtils.toLong(Instant.from(asDateTime(DateUtils.toInstant(dateNanos), zoneId).minus(temporalAmount))); } catch (IllegalArgumentException e) { /* toLong will throw IllegalArgumentException for out of range dates, but that includes the actual value which we want @@ -177,4 +179,14 @@ public Period fold(Period left, Period right) { public Duration fold(Duration left, Duration right) { return left.minus(right); } + + @Override + public Configuration configuration() { + return configuration; + } + + @Override + public Sub withConfiguration(Configuration configuration) { + return new Sub(source(), left(), right(), configuration); + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PostOptimizationPhasePlanVerifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PostOptimizationPhasePlanVerifier.java index 82e8ae6543109..6613f0932dda5 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PostOptimizationPhasePlanVerifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PostOptimizationPhasePlanVerifier.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.optimizer; +import org.elasticsearch.xpack.esql.capabilities.ConfigurationAware; import org.elasticsearch.xpack.esql.common.Failures; import org.elasticsearch.xpack.esql.core.expression.Alias; import org.elasticsearch.xpack.esql.core.expression.Attribute; @@ -49,6 +50,8 @@ public Failures verify(P optimizedPlan, List expectedOutputAttributes verifyOutputNotChanged(optimizedPlan, expectedOutputAttributes, failures); + ConfigurationAware.verifyNoMarkerConfiguration(optimizedPlan, failures); + if (depFailures.hasFailures()) { throw new IllegalStateException(depFailures.toString()); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SimplifyComparisonsArithmetics.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SimplifyComparisonsArithmetics.java index 60ff161651f2d..2a47567762c54 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SimplifyComparisonsArithmetics.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/SimplifyComparisonsArithmetics.java @@ -47,7 +47,7 @@ protected Expression rule(BinaryComparison bc, LogicalOptimizerContext ctx) { // optimize only once the expression has a literal on the right side of the binary comparison if (bc.right() instanceof Literal) { if (bc.left() instanceof ArithmeticOperation) { - return simplifyBinaryComparison(ctx.foldCtx(), bc); + return simplifyBinaryComparison(ctx, bc); } if (bc.left() instanceof Neg) { return foldNegation(ctx.foldCtx(), bc); @@ -56,7 +56,7 @@ protected Expression rule(BinaryComparison bc, LogicalOptimizerContext ctx) { return bc; } - private Expression simplifyBinaryComparison(FoldContext foldContext, BinaryComparison comparison) { + private Expression simplifyBinaryComparison(LogicalOptimizerContext ctx, BinaryComparison comparison) { ArithmeticOperation operation = (ArithmeticOperation) comparison.left(); // Use symbol comp: SQL operations aren't available in this package (as dependencies) String opSymbol = operation.symbol(); @@ -66,9 +66,9 @@ private Expression simplifyBinaryComparison(FoldContext foldContext, BinaryCompa } OperationSimplifier simplification = null; if (isMulOrDiv(opSymbol)) { - simplification = new MulDivSimplifier(foldContext, comparison); + simplification = new MulDivSimplifier(ctx, comparison); } else if (opSymbol.equals(ADD.symbol()) || opSymbol.equals(SUB.symbol())) { - simplification = new AddSubSimplifier(foldContext, comparison); + simplification = new AddSubSimplifier(ctx, comparison); } return (simplification == null || simplification.isUnsafe(typesCompatible)) ? comparison : simplification.apply(); @@ -97,7 +97,7 @@ private static Expression tryFolding(FoldContext ctx, Expression expression) { } private abstract static class OperationSimplifier { - final FoldContext foldContext; + final LogicalOptimizerContext ctx; final BinaryComparison comparison; final Literal bcLiteral; final ArithmeticOperation operation; @@ -105,8 +105,8 @@ private abstract static class OperationSimplifier { final Expression opRight; final Literal opLiteral; - OperationSimplifier(FoldContext foldContext, BinaryComparison comparison) { - this.foldContext = foldContext; + OperationSimplifier(LogicalOptimizerContext ctx, BinaryComparison comparison) { + this.ctx = ctx; this.comparison = comparison; operation = (ArithmeticOperation) comparison.left(); bcLiteral = (Literal) comparison.right(); @@ -155,7 +155,7 @@ final Expression apply() { Expression bcRightExpression = ((BinaryComparisonInversible) operation).binaryComparisonInverse() .create(bcl.source(), bcl, opRight); - bcRightExpression = tryFolding(foldContext, bcRightExpression); + bcRightExpression = tryFolding(ctx.foldCtx(), bcRightExpression); return bcRightExpression != null ? postProcess((BinaryComparison) comparison.replaceChildren(List.of(opLeft, bcRightExpression))) : comparison; @@ -173,8 +173,8 @@ Expression postProcess(BinaryComparison binaryComparison) { private static class AddSubSimplifier extends OperationSimplifier { - AddSubSimplifier(FoldContext foldContext, BinaryComparison comparison) { - super(foldContext, comparison); + AddSubSimplifier(LogicalOptimizerContext ctx, BinaryComparison comparison) { + super(ctx, comparison); } @Override @@ -186,7 +186,7 @@ boolean isOpUnsafe() { if (operation.symbol().equals(SUB.symbol()) && opRight instanceof Literal == false) { // such as: 1 - x > -MAX // if next simplification step would fail on overflow anyways, skip the optimisation already - return tryFolding(foldContext, new Sub(EMPTY, opLeft, bcLiteral)) == null; + return tryFolding(ctx.foldCtx(), new Sub(EMPTY, opLeft, bcLiteral, ctx.configuration())) == null; } return false; @@ -198,8 +198,8 @@ private static class MulDivSimplifier extends OperationSimplifier { private final boolean isDiv; // and not MUL. private final int opRightSign; // sign of the right operand in: (left) (op) (right) (comp) (literal) - MulDivSimplifier(FoldContext foldContext, BinaryComparison comparison) { - super(foldContext, comparison); + MulDivSimplifier(LogicalOptimizerContext ctx, BinaryComparison comparison) { + super(ctx, comparison); isDiv = operation.symbol().equals(DIV.symbol()); opRightSign = sign(opRight); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/promql/TranslatePromqlToTimeSeriesAggregate.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/promql/TranslatePromqlToTimeSeriesAggregate.java index 7050e1a4fd85d..0caad3c2e4ef8 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/promql/TranslatePromqlToTimeSeriesAggregate.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/promql/TranslatePromqlToTimeSeriesAggregate.java @@ -38,6 +38,7 @@ import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThanOrEqual; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.NotEquals; import org.elasticsearch.xpack.esql.expression.promql.function.PromqlFunctionRegistry; +import org.elasticsearch.xpack.esql.optimizer.LogicalOptimizerContext; import org.elasticsearch.xpack.esql.optimizer.rules.logical.OptimizerRules; import org.elasticsearch.xpack.esql.optimizer.rules.logical.TranslateTimeSeriesAggregate; import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.IgnoreNullMetrics; @@ -83,7 +84,9 @@ * └── EsRelation(*, mode=TIME_SERIES) * */ -public final class TranslatePromqlToTimeSeriesAggregate extends OptimizerRules.OptimizerRule { +public final class TranslatePromqlToTimeSeriesAggregate extends OptimizerRules.ParameterizedOptimizerRule< + PromqlCommand, + LogicalOptimizerContext> { // TODO make configurable via lookback_delta parameter and (cluster?) setting public static final Duration DEFAULT_LOOKBACK = Duration.ofMinutes(5); @@ -94,7 +97,7 @@ public TranslatePromqlToTimeSeriesAggregate() { } @Override - protected LogicalPlan rule(PromqlCommand promqlCommand) { + protected LogicalPlan rule(PromqlCommand promqlCommand, LogicalOptimizerContext context) { // Safety check: this should never occur as the parser should reject PromQL when disabled, // but we check here as an additional safety measure if (PromqlFeatures.isEnabled() == false) { @@ -104,7 +107,7 @@ protected LogicalPlan rule(PromqlCommand promqlCommand) { } List labelFilterConditions = new ArrayList<>(); - Expression value = mapNode(promqlCommand, promqlCommand.promqlPlan(), labelFilterConditions); + Expression value = mapNode(promqlCommand, promqlCommand.promqlPlan(), labelFilterConditions, context); LogicalPlan plan = withTimestampFilter(promqlCommand, promqlCommand.child()); plan = addLabelFilters(promqlCommand, labelFilterConditions, plan); plan = createTimeSeriesAggregate(promqlCommand, value, plan); @@ -210,14 +213,20 @@ private static LogicalPlan convertValueToDouble(PromqlCommand promqlCommand, Log * Recursively maps PromQL plan nodes to ESQL expressions to compute the value for the time series aggregation. * Collects label filter conditions into the provided list. */ - private static Expression mapNode(PromqlCommand promqlCommand, LogicalPlan p, List labelFilterConditions) { + private static Expression mapNode( + PromqlCommand promqlCommand, + LogicalPlan p, + List labelFilterConditions, + LogicalOptimizerContext context + ) { return switch (p) { case Selector selector -> mapSelector(promqlCommand, selector, labelFilterConditions); - case PromqlFunctionCall functionCall -> mapFunction(promqlCommand, functionCall, labelFilterConditions); + case PromqlFunctionCall functionCall -> mapFunction(promqlCommand, functionCall, labelFilterConditions, context); case VectorBinaryArithmetic vectorBinaryArithmetic -> mapVectorBinaryArithmetic( promqlCommand, vectorBinaryArithmetic, - labelFilterConditions + labelFilterConditions, + context ); default -> throw new QlIllegalArgumentException("Unsupported PromQL plan node: {}", p); }; @@ -231,15 +240,16 @@ private static Expression mapNode(PromqlCommand promqlCommand, LogicalPlan p, Li private static Expression mapVectorBinaryArithmetic( PromqlCommand promqlCommand, VectorBinaryArithmetic vectorBinaryArithmetic, - List labelFilterConditions + List labelFilterConditions, + LogicalOptimizerContext context ) { - Expression left = mapNode(promqlCommand, vectorBinaryArithmetic.left(), labelFilterConditions); + Expression left = mapNode(promqlCommand, vectorBinaryArithmetic.left(), labelFilterConditions, context); left = new ToDouble(left.source(), left); - Expression right = mapNode(promqlCommand, vectorBinaryArithmetic.right(), labelFilterConditions); + Expression right = mapNode(promqlCommand, vectorBinaryArithmetic.right(), labelFilterConditions, context); right = new ToDouble(right.source(), right); - return vectorBinaryArithmetic.binaryOp().asFunction().create(vectorBinaryArithmetic.source(), left, right); + return vectorBinaryArithmetic.binaryOp().asFunction().create(vectorBinaryArithmetic.source(), left, right, context.configuration()); } /** @@ -272,9 +282,10 @@ private static Expression mapSelector(PromqlCommand promqlCommand, Selector sele private static Expression mapFunction( PromqlCommand promqlCommand, PromqlFunctionCall functionCall, - List labelFilterConditions + List labelFilterConditions, + LogicalOptimizerContext context ) { - Expression target = mapNode(promqlCommand, functionCall.child(), labelFilterConditions); + Expression target = mapNode(promqlCommand, functionCall.child(), labelFilterConditions, context); final Expression window; if (functionCall.child() instanceof RangeSelector rangeSelector) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java index 87b4eceb23878..febfe44c3f3e9 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/ExpressionBuilder.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.regex.Regex; +import org.elasticsearch.xpack.esql.capabilities.ConfigurationAware; import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Alias; import org.elasticsearch.xpack.esql.core.expression.Attribute; @@ -608,8 +609,8 @@ public Expression visitArithmeticBinary(EsqlBaseParser.ArithmeticBinaryContext c case EsqlBaseParser.ASTERISK -> new Mul(source, left, right); case EsqlBaseParser.SLASH -> new Div(source, left, right); case EsqlBaseParser.PERCENT -> new Mod(source, left, right); - case EsqlBaseParser.PLUS -> new Add(source, left, right); - case EsqlBaseParser.MINUS -> new Sub(source, left, right); + case EsqlBaseParser.PLUS -> new Add(source, left, right, ConfigurationAware.CONFIGURATION_MARKER); + case EsqlBaseParser.MINUS -> new Sub(source, left, right, ConfigurationAware.CONFIGURATION_MARKER); default -> throw new ParsingException(source, "Unknown arithmetic operator {}", source.text()); }; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryArithmetic.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryArithmetic.java index 4779638a5b69b..636214021ce67 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryArithmetic.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryArithmetic.java @@ -32,10 +32,10 @@ public ScalarFunctionFactory asFunction() { return switch (this) { case ADD -> Add::new; case SUB -> Sub::new; - case MUL -> Mul::new; - case DIV -> Div::new; - case MOD -> Mod::new; - case POW -> Pow::new; + case MUL -> (s, l, r, c) -> new Mul(s, l, r); + case DIV -> (s, l, r, c) -> new Div(s, l, r); + case MOD -> (s, l, r, c) -> new Mod(s, l, r); + case POW -> (s, l, r, c) -> new Pow(s, l, r); }; } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryComparison.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryComparison.java index 1f66659bfa791..c2add3df1cd53 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryComparison.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryComparison.java @@ -32,12 +32,12 @@ public enum ComparisonOp implements BinaryOp { @Override public ScalarFunctionFactory asFunction() { return switch (this) { - case EQ -> Equals::new; - case NEQ -> NotEquals::new; - case GT -> GreaterThan::new; - case GTE -> GreaterThanOrEqual::new; - case LT -> LessThan::new; - case LTE -> LessThanOrEqual::new; + case EQ -> (s, l, r, c) -> new Equals(s, l, r); + case NEQ -> (s, l, r, c) -> new NotEquals(s, l, r); + case GT -> (s, l, r, c) -> new GreaterThan(s, l, r); + case GTE -> (s, l, r, c) -> new GreaterThanOrEqual(s, l, r); + case LT -> (s, l, r, c) -> new LessThan(s, l, r); + case LTE -> (s, l, r, c) -> new LessThanOrEqual(s, l, r); }; } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryOperator.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryOperator.java index 166ef29970498..987e09f059896 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryOperator.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/operator/VectorBinaryOperator.java @@ -17,6 +17,7 @@ import org.elasticsearch.xpack.esql.plan.logical.BinaryPlan; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.esql.plan.logical.promql.selector.LabelMatcher; +import org.elasticsearch.xpack.esql.session.Configuration; import java.io.IOException; import java.util.ArrayList; @@ -44,7 +45,7 @@ public interface BinaryOp { } public interface ScalarFunctionFactory { - Function create(Source source, Expression left, Expression right); + Function create(Source source, Expression left, Expression right, Configuration configuration); } protected VectorBinaryOperator( diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java index 2351b13abe990..00f3c21689803 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java @@ -79,6 +79,7 @@ import org.elasticsearch.xpack.esql.expression.function.vector.VectorSimilarityFunction; import org.elasticsearch.xpack.esql.expression.predicate.nulls.IsNotNull; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add; +import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Sub; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.GreaterThan; import org.elasticsearch.xpack.esql.index.EsIndex; @@ -111,6 +112,7 @@ import org.elasticsearch.xpack.esql.plan.logical.inference.Rerank; import org.elasticsearch.xpack.esql.plan.logical.join.LookupJoin; import org.elasticsearch.xpack.esql.plan.logical.local.EsqlProject; +import org.elasticsearch.xpack.esql.session.Configuration; import org.elasticsearch.xpack.esql.session.IndexResolver; import java.io.IOException; @@ -5752,6 +5754,28 @@ public void testRLikeListParameters() { } } + public void testConfigurationAwareResolved() { + var query = """ + from test + | eval a = hire_date + 1d, b = hire_date - 1d + """; + Configuration configuration = configuration(query); + var analyzer = analyzer( + Map.of(new IndexPattern(Source.EMPTY, "test"), loadMapping("mapping-basic.json", "test")), + TEST_VERIFIER, + configuration + ); + var plan = analyze(query, analyzer); + + var limit = as(plan, Limit.class); + var eval = as(limit.child(), Eval.class); + assertThat(Expressions.names(eval.fields()), is(List.of("a", "b"))); + var add = as(Alias.unwrap(eval.fields().get(0)), Add.class); + var sub = as(Alias.unwrap(eval.fields().get(1)), Sub.class); + assertThat(add.configuration(), is(configuration)); + assertThat(sub.configuration(), is(configuration)); + } + private void verifyNameAndTypeAndMultiTypeEsField( String actualName, DataType actualType, diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/evaluator/mapper/EvaluatorMapperTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/evaluator/mapper/EvaluatorMapperTests.java index 828f9e061686b..c77698c523b0a 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/evaluator/mapper/EvaluatorMapperTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/evaluator/mapper/EvaluatorMapperTests.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.esql.evaluator.mapper; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.core.expression.FoldContext; import org.elasticsearch.xpack.esql.core.expression.Literal; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -20,7 +21,8 @@ public void testFoldCompletesWithPlentyOfMemory() { Add add = new Add( Source.synthetic("shouldn't break"), new Literal(Source.EMPTY, 1, DataType.INTEGER), - new Literal(Source.EMPTY, 3, DataType.INTEGER) + new Literal(Source.EMPTY, 3, DataType.INTEGER), + EsqlTestUtils.TEST_CFG ); assertEquals(add.fold(new FoldContext(100)), 4); } @@ -29,7 +31,8 @@ public void testFoldBreaksWithLittleMemory() { Add add = new Add( Source.synthetic("should break"), new Literal(Source.EMPTY, 1, DataType.INTEGER), - new Literal(Source.EMPTY, 3, DataType.INTEGER) + new Literal(Source.EMPTY, 3, DataType.INTEGER), + EsqlTestUtils.TEST_CFG ); Exception e = expectThrows(FoldContext.FoldTooMuchMemoryException.class, () -> add.fold(new FoldContext(10))); assertThat( diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/CanonicalTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/CanonicalTests.java index da076a5db031b..099bf1e83df3d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/CanonicalTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/CanonicalTests.java @@ -41,6 +41,7 @@ import java.util.function.Function; import static java.util.Arrays.asList; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.esql.EsqlTestUtils.equalsOf; import static org.elasticsearch.xpack.esql.EsqlTestUtils.fieldAttribute; import static org.elasticsearch.xpack.esql.EsqlTestUtils.greaterThanOf; @@ -56,19 +57,29 @@ public class CanonicalTests extends ESTestCase { public void testNonCommutativeBinary() throws Exception { Div div = new Div(EMPTY, of(2), of(1)); - Sub sub = new Sub(EMPTY, of(2), of(1)); + Sub sub = new Sub(EMPTY, of(2), of(1), TEST_CFG); Mod mod = new Mod(EMPTY, div, sub); assertEquals(mod, mod.canonical()); } public void testNonCommutativeMixedWithCommutative() throws Exception { // (0+1) / 1 - Div div = new Div(EMPTY, new Add(EMPTY, of(0), of(1)), of(1)); + Div div = new Div(EMPTY, new Add(EMPTY, of(0), of(1), TEST_CFG), of(1)); // 1*2 - 1+2 - Sub sub = new Sub(EMPTY, new Mul(EMPTY, of(1), new Add(EMPTY, of(1), of(2))), new Add(EMPTY, of(1), of(2))); + Sub sub = new Sub( + EMPTY, + new Mul(EMPTY, of(1), new Add(EMPTY, of(1), of(2), TEST_CFG)), + new Add(EMPTY, of(1), of(2), TEST_CFG), + TEST_CFG + ); - Div shuffledDiv = new Div(EMPTY, new Add(EMPTY, of(1), of(0)), of(1)); - Sub shuffledSub = new Sub(EMPTY, new Mul(EMPTY, new Add(EMPTY, of(2), of(1)), of(1)), new Add(EMPTY, of(2), of(1))); + Div shuffledDiv = new Div(EMPTY, new Add(EMPTY, of(1), of(0), TEST_CFG), of(1)); + Sub shuffledSub = new Sub( + EMPTY, + new Mul(EMPTY, new Add(EMPTY, of(2), of(1), TEST_CFG), of(1)), + new Add(EMPTY, of(2), of(1), TEST_CFG), + TEST_CFG + ); And and = new And(EMPTY, div, sub); And shuffledAnd = new And(EMPTY, shuffledDiv, shuffledSub); @@ -90,16 +101,16 @@ public void testAndManually() throws Exception { } public void testBasicSymmetricalAdd() throws Exception { - Expression left = new Add(EMPTY, new Add(EMPTY, of(1), of(2)), new Add(EMPTY, of(3), of(4))); - Expression right = new Add(EMPTY, new Add(EMPTY, of(4), of(2)), new Add(EMPTY, of(1), of(3))); + Expression left = new Add(EMPTY, new Add(EMPTY, of(1), of(2), TEST_CFG), new Add(EMPTY, of(3), of(4), TEST_CFG), TEST_CFG); + Expression right = new Add(EMPTY, new Add(EMPTY, of(4), of(2), TEST_CFG), new Add(EMPTY, of(1), of(3), TEST_CFG), TEST_CFG); assertEquals(left.canonical(), right.canonical()); assertEquals(left.semanticHash(), right.semanticHash()); } public void testBasicASymmetricalAdd() throws Exception { - Expression left = new Add(EMPTY, new Add(EMPTY, of(1), of(2)), of(3)); - Expression right = new Add(EMPTY, of(1), new Add(EMPTY, of(2), of(3))); + Expression left = new Add(EMPTY, new Add(EMPTY, of(1), of(2), TEST_CFG), of(3), TEST_CFG); + Expression right = new Add(EMPTY, of(1), new Add(EMPTY, of(2), of(3), TEST_CFG), TEST_CFG); assertEquals(left.canonical(), right.canonical()); assertEquals(left.semanticHash(), right.semanticHash()); @@ -129,14 +140,14 @@ public void testBinaryOperatorCombinations() throws Exception { FieldAttribute d = fieldAttribute(); And ab = new And(EMPTY, greaterThanOf(a, of(1)), lessThanOf(b, of(2))); - And cd = new And(EMPTY, equalsOf(new Add(EMPTY, c, of(20)), of(3)), greaterThanOrEqualOf(d, of(4))); + And cd = new And(EMPTY, equalsOf(new Add(EMPTY, c, of(20), TEST_CFG), of(3)), greaterThanOrEqualOf(d, of(4))); And and = new And(EMPTY, ab, cd); // swap d comparison And db = new And(EMPTY, greaterThanOrEqualOf(d, of(4)).swapLeftAndRight(), lessThanOf(b, of(2))); // swap order for c and swap a comparison - And ca = new And(EMPTY, equalsOf(new Add(EMPTY, of(20), c), of(3)), greaterThanOf(a, of(1))); + And ca = new And(EMPTY, equalsOf(new Add(EMPTY, of(20), c, TEST_CFG), of(3)), greaterThanOf(a, of(1))); And shuffleAnd = new And(EMPTY, db, ca); @@ -150,13 +161,13 @@ public void testNot() throws Exception { FieldAttribute d = fieldAttribute(); And ab = new And(EMPTY, greaterThanOf(a, of(1)), lessThanOf(b, of(2))); - And cd = new And(EMPTY, equalsOf(new Add(EMPTY, c, of(20)), of(3)), greaterThanOrEqualOf(d, of(4))); + And cd = new And(EMPTY, equalsOf(new Add(EMPTY, c, of(20), TEST_CFG), of(3)), greaterThanOrEqualOf(d, of(4))); And and = new And(EMPTY, ab, cd); // swap d comparison Or db = new Or(EMPTY, new Not(EMPTY, greaterThanOrEqualOf(d, of(4))), lessThanOf(b, of(2)).negate()); // swap order for c and swap a comparison - Or ca = new Or(EMPTY, notEqualsOf(new Add(EMPTY, of(20), c), of(3)), new Not(EMPTY, greaterThanOf(a, of(1)))); + Or ca = new Or(EMPTY, notEqualsOf(new Add(EMPTY, of(20), c, TEST_CFG), of(3)), new Not(EMPTY, greaterThanOf(a, of(1)))); Not not = new Not(EMPTY, new Or(EMPTY, db, ca)); @@ -212,7 +223,7 @@ interface BinaryOperatorFactory { public void testBasicOperators() throws Exception { List list = Arrays.asList( // arithmetic - Add::new, + (source, left, right) -> new Add(source, left, right, TEST_CFG), Mul::new, // logical Or::new, diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/NamedExpressionTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/NamedExpressionTests.java index 00afd4947c890..a507bf1a5471c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/NamedExpressionTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/NamedExpressionTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Sub; import static java.util.Collections.emptyMap; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.esql.EsqlTestUtils.of; import static org.elasticsearch.xpack.esql.core.tree.Source.EMPTY; @@ -28,7 +29,7 @@ public class NamedExpressionTests extends ESTestCase { public void testArithmeticFunctionName() { String e = "5 + 2"; - Add add = new Add(s(e), l(5), l(2)); + Add add = new Add(s(e), l(5), l(2), TEST_CFG); assertEquals(e, add.sourceText()); e = "5 / 2"; @@ -44,7 +45,7 @@ public void testArithmeticFunctionName() { assertEquals(e, mul.sourceText()); e = "5 -2"; - Sub sub = new Sub(s(e), l(5), l(2)); + Sub sub = new Sub(s(e), l(5), l(2), TEST_CFG); assertEquals(e, sub.sourceText()); e = " - 5"; @@ -59,7 +60,7 @@ public void testNameForArithmeticFunctionAppliedOnTableColumn() { new EsField("myESField", DataType.INTEGER, emptyMap(), true, EsField.TimeSeriesFieldType.NONE) ); String e = "myField + 10"; - Add add = new Add(s(e), fa, l(10)); + Add add = new Add(s(e), fa, l(10), TEST_CFG); assertEquals(e, add.sourceText()); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddSerializationTests.java index b8924a01d0904..a55b9a6e1f319 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddSerializationTests.java @@ -13,6 +13,6 @@ public class AddSerializationTests extends AbstractArithmeticSerializationTests { @Override protected Add create(Source source, Expression left, Expression right) { - return new Add(source, left, right); + return new Add(source, left, right, configuration()); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddTests.java index 40111410e95f8..5919bc6e13073 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/AddTests.java @@ -14,14 +14,16 @@ 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 java.math.BigInteger; import java.time.Duration; import java.time.Instant; import java.time.Period; -import java.time.ZonedDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.temporal.TemporalAmount; import java.util.ArrayList; import java.util.List; @@ -29,16 +31,18 @@ import java.util.function.BiFunction; import java.util.function.BinaryOperator; import java.util.function.Supplier; -import java.util.function.ToLongBiFunction; +import static org.elasticsearch.test.ReadableMatchers.matchesDateMillis; +import static org.elasticsearch.test.ReadableMatchers.matchesDateNanos; import static org.elasticsearch.xpack.esql.core.util.DateUtils.asDateTime; import static org.elasticsearch.xpack.esql.core.util.DateUtils.asMillis; import static org.elasticsearch.xpack.esql.core.util.NumericUtils.asLongUnsigned; +import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; -public class AddTests extends AbstractScalarFunctionTestCase { +public class AddTests extends AbstractConfigurationFunctionTestCase { public AddTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); } @@ -48,7 +52,7 @@ public static Iterable parameters() { List suppliers = new ArrayList<>(); suppliers.addAll( TestCaseSupplier.forBinaryWithWidening( - new TestCaseSupplier.NumericTypeTestConfigs( + new TestCaseSupplier.NumericTypeTestConfigs<>( new TestCaseSupplier.NumericTypeTestConfig<>( (Integer.MIN_VALUE >> 1) - 1, (Integer.MAX_VALUE >> 1) - 1, @@ -151,14 +155,14 @@ public static Iterable parameters() { BinaryOperator result = (lhs, rhs) -> { try { - return addDatesAndTemporalAmount(lhs, rhs, AddTests::addMillis); + return addDatesAndTemporalAmount(lhs, rhs, ZoneOffset.UTC, AddTests::addMillis); } catch (ArithmeticException e) { return null; } }; BiFunction> warnings = (lhs, rhs) -> { try { - addDatesAndTemporalAmount(lhs.getValue(), rhs.getValue(), AddTests::addMillis); + addDatesAndTemporalAmount(lhs.getValue(), rhs.getValue(), ZoneOffset.UTC, AddTests::addMillis); return List.of(); } catch (ArithmeticException e) { return List.of( @@ -193,7 +197,7 @@ public static Iterable parameters() { BinaryOperator nanosResult = (lhs, rhs) -> { try { assert (lhs instanceof Instant) || (rhs instanceof Instant); - return addDatesAndTemporalAmount(lhs, rhs, AddTests::addNanos); + return addDatesAndTemporalAmount(lhs, rhs, ZoneOffset.UTC, AddTests::addNanos); } catch (ArithmeticException e) { return null; } @@ -288,6 +292,21 @@ public static Iterable parameters() { ) ); + // Set the timezone to UTC for test cases up to here + suppliers = TestCaseSupplier.mapTestCases( + suppliers, + tc -> tc.withConfiguration(TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)) + ); + + // Date tests with timezones + // -5 to -4 at 2025-03-09T02:00:00-05:00 + suppliers.addAll(suppliersForDate("2025-03-09T01:00:00-05:00", Period.ofDays(1), "Z", "2025-03-10T01:00:00-05:00")); + suppliers.addAll(suppliersForDate("2025-03-09T01:00:00-05:00", Period.ofDays(1), "America/New_York", "2025-03-10T01:00:00-04:00")); + // 24h should do nothing for timezones + suppliers.addAll( + suppliersForDate("2025-03-09T01:00:00-05:00", Duration.ofHours(24), "America/New_York", "2025-03-10T01:00:00-05:00") + ); + // Datetime tests are split in two, depending on their permissiveness of null-injection, which cannot happen "automatically" for // Datetime + Period/Duration, since the expression will take the non-null arg's type. suppliers = anyNullIsNull( @@ -311,7 +330,53 @@ private static String addErrorMessageString(boolean includeOrdinal, List adder) { + private static List suppliersForDate( + String dateString, + TemporalAmount period, + String zoneIdString, + String expectedResultString + ) { + Instant inputDate = Instant.parse(dateString); + long dateAsMillis = DateUtils.toLongMillis(inputDate); + long dateAsNanos = DateUtils.toLong(inputDate); + DataType periodType = period instanceof Period ? DataType.DATE_PERIOD : DataType.TIME_DURATION; + ZoneId zoneId = ZoneId.of(zoneIdString); + + return List.of( + new TestCaseSupplier( + "millis " + dateString + ", " + period + ", " + zoneIdString, + List.of(DataType.DATETIME, periodType), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(dateAsMillis, DataType.DATETIME, "date"), + new TestCaseSupplier.TypedData(period, periodType, "period").forceLiteral() + ), + "AddDatetimesEvaluator[datetime=Attribute[channel=0], temporalAmount=" + period + ", zoneId=" + zoneId + "]", + DataType.DATETIME, + matchesDateMillis(expectedResultString) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) + ), + new TestCaseSupplier( + "nanos " + dateString + ", " + period + ", " + zoneIdString, + List.of(DataType.DATE_NANOS, periodType), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(dateAsNanos, DataType.DATE_NANOS, "date"), + new TestCaseSupplier.TypedData(period, periodType, "period").forceLiteral() + ), + "AddDateNanosEvaluator[dateNanos=Attribute[channel=0], temporalAmount=" + period + ", zoneId=" + zoneId + "]", + DataType.DATE_NANOS, + matchesDateNanos(expectedResultString) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) + ) + ); + } + + interface DateAdder { + long add(Instant date, TemporalAmount period, ZoneId zoneId); + } + + private static Object addDatesAndTemporalAmount(Object lhs, Object rhs, ZoneId zoneId, DateAdder adder) { // this weird casting dance makes the expected value lambda symmetric Instant date; TemporalAmount period; @@ -323,21 +388,19 @@ private static Object addDatesAndTemporalAmount(Object lhs, Object rhs, ToLongBi date = (Instant) rhs; period = (TemporalAmount) lhs; } - return adder.applyAsLong(date, period); + return adder.add(date, period, zoneId); } - private static long addMillis(Instant date, TemporalAmount period) { - return asMillis(asDateTime(date).plus(period)); + private static long addMillis(Instant date, TemporalAmount period, ZoneId zoneId) { + return asMillis(asDateTime(date, zoneId).plus(period)); } - private static long addNanos(Instant date, TemporalAmount period) { - return DateUtils.toLong( - Instant.from(ZonedDateTime.ofInstant(date, org.elasticsearch.xpack.esql.core.util.DateUtils.UTC).plus(period)) - ); + private static long addNanos(Instant date, TemporalAmount period, ZoneId zoneId) { + return DateUtils.toLong(Instant.from(asDateTime(date, zoneId).plus(period))); } @Override - protected Expression build(Source source, List args) { - return new Add(source, args.get(0), args.get(1)); + protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { + return new Add(source, args.get(0), args.get(1), configuration); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubSerializationTests.java index 274d7f669b2aa..09d277907e8e7 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubSerializationTests.java @@ -13,6 +13,6 @@ public class SubSerializationTests extends AbstractArithmeticSerializationTests { @Override protected Sub create(Source source, Expression left, Expression right) { - return new Sub(source, left, right); + return new Sub(source, left, right, configuration()); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubTests.java index 54ef4eb7563d2..e4cf27c61f829 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/predicate/operator/arithmetic/SubTests.java @@ -14,13 +14,16 @@ 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; import java.time.Instant; import java.time.Period; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.TemporalAmount; import java.util.List; @@ -29,14 +32,17 @@ import java.util.function.Supplier; import java.util.function.ToLongBiFunction; +import static org.elasticsearch.test.ReadableMatchers.matchesDateMillis; +import static org.elasticsearch.test.ReadableMatchers.matchesDateNanos; import static org.elasticsearch.xpack.esql.EsqlTestUtils.randomLiteral; import static org.elasticsearch.xpack.esql.core.util.DateUtils.asDateTime; import static org.elasticsearch.xpack.esql.core.util.DateUtils.asMillis; import static org.elasticsearch.xpack.esql.core.util.NumericUtils.ZERO_AS_UNSIGNED_LONG; +import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TEST_SOURCE; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.startsWith; -public class SubTests extends AbstractScalarFunctionTestCase { +public class SubTests extends AbstractConfigurationFunctionTestCase { public SubTests(@Name("TestCase") Supplier testCaseSupplier) { this.testCase = testCaseSupplier.get(); } @@ -128,7 +134,7 @@ public static Iterable parameters() { ), Matchers.startsWith("SubDatetimesEvaluator[datetime=Attribute[channel=0], temporalAmount="), DataType.DATETIME, - equalTo(asMillis(asDateTime(lhs).minus(rhs))) + equalTo(asMillis(asDateTime(lhs, ZoneOffset.UTC).minus(rhs))) ); })); @@ -185,7 +191,7 @@ public static Iterable parameters() { ), Matchers.startsWith("SubDatetimesEvaluator[datetime=Attribute[channel=0], temporalAmount="), DataType.DATETIME, - equalTo(asMillis(asDateTime(lhs).minus(rhs))) + equalTo(asMillis(asDateTime(lhs, ZoneOffset.UTC).minus(rhs))) ); return testCase; })); @@ -245,6 +251,21 @@ public static Iterable parameters() { ) ); + // Set the timezone to UTC for test cases up to here + suppliers = TestCaseSupplier.mapTestCases( + suppliers, + tc -> tc.withConfiguration(TEST_SOURCE, configurationForTimezone(ZoneOffset.UTC)) + ); + + // Date tests with timezones + // -5 to -4 at 2025-03-09T02:00:00-05:00 + suppliers.addAll(suppliersForDate("2025-03-10T01:00:00-05:00", Period.ofDays(1), "Z", "2025-03-09T01:00:00-05:00")); + suppliers.addAll(suppliersForDate("2025-03-10T01:00:00-04:00", Period.ofDays(1), "America/New_York", "2025-03-09T01:00:00-05:00")); + // 24h should do nothing for timezones + suppliers.addAll( + suppliersForDate("2025-03-10T01:00:00-05:00", Duration.ofHours(24), "America/New_York", "2025-03-09T01:00:00-05:00") + ); + suppliers = errorsForCasesWithoutExamples(anyNullIsNull(suppliers, (nullPosition, nullValueDataType, original) -> { if (nullValueDataType == DataType.NULL) { return original.getData().get(nullPosition == 0 ? 1 : 0).type(); @@ -279,9 +300,51 @@ private static String subErrorMessageString(boolean includeOrdinal, List suppliersForDate( + String dateString, + TemporalAmount period, + String zoneIdString, + String expectedResultString + ) { + Instant inputDate = Instant.parse(dateString); + long dateAsMillis = DateUtils.toLongMillis(inputDate); + long dateAsNanos = DateUtils.toLong(inputDate); + DataType periodType = period instanceof Period ? DataType.DATE_PERIOD : DataType.TIME_DURATION; + ZoneId zoneId = ZoneId.of(zoneIdString); + + return List.of( + new TestCaseSupplier( + "millis " + dateString + ", " + period + ", " + zoneIdString, + List.of(DataType.DATETIME, periodType), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(dateAsMillis, DataType.DATETIME, "date"), + new TestCaseSupplier.TypedData(period, periodType, "period").forceLiteral() + ), + "SubDatetimesEvaluator[datetime=Attribute[channel=0], temporalAmount=" + period + ", zoneId=" + zoneId + "]", + DataType.DATETIME, + matchesDateMillis(expectedResultString) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) + ), + new TestCaseSupplier( + "nanos " + dateString + ", " + period + ", " + zoneIdString, + List.of(DataType.DATE_NANOS, periodType), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(dateAsNanos, DataType.DATE_NANOS, "date"), + new TestCaseSupplier.TypedData(period, periodType, "period").forceLiteral() + ), + "SubDateNanosEvaluator[dateNanos=Attribute[channel=0], temporalAmount=" + period + ", zoneId=" + zoneId + "]", + DataType.DATE_NANOS, + matchesDateNanos(expectedResultString) + ).withConfiguration(TEST_SOURCE, configurationForTimezone(zoneId)) + ) + ); + } + @Override - protected Expression build(Source source, List args) { - return new Sub(source, args.get(0), args.get(1)); + protected Expression buildWithConfiguration(Source source, List args, Configuration configuration) { + return new Sub(source, args.get(0), args.get(1), configuration); } private static Object subtractDatesAndTemporalAmount(Object lhs, Object rhs, ToLongBiFunction subtract) { 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 f9470324f202a..2d3e7a331e443 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 @@ -568,7 +568,7 @@ public void testIsNotNullOnIsNullField() { public void testIsNotNullOnOperatorWithOneField() { EsRelation relation = relation(); var fieldA = getFieldAttribute("a"); - Expression inn = isNotNull(new Add(EMPTY, fieldA, ONE)); + Expression inn = isNotNull(new Add(EMPTY, fieldA, ONE, TEST_CFG)); Filter f = new Filter(EMPTY, relation, inn); Filter expected = new Filter(EMPTY, relation, new And(EMPTY, isNotNull(fieldA), inn)); @@ -579,7 +579,7 @@ public void testIsNotNullOnOperatorWithTwoFields() { EsRelation relation = relation(); var fieldA = getFieldAttribute("a"); var fieldB = getFieldAttribute("b"); - Expression inn = isNotNull(new Add(EMPTY, fieldA, fieldB)); + Expression inn = isNotNull(new Add(EMPTY, fieldA, fieldB, TEST_CFG)); Filter f = new Filter(EMPTY, relation, inn); Filter expected = new Filter(EMPTY, relation, new And(EMPTY, new And(EMPTY, isNotNull(fieldA), isNotNull(fieldB)), inn)); @@ -590,7 +590,9 @@ public void testIsNotNullOnFunctionWithOneField() { EsRelation relation = relation(); var fieldA = getFieldAttribute("a"); var pattern = L("abc"); - Expression inn = isNotNull(new And(EMPTY, new StartsWith(EMPTY, fieldA, pattern), greaterThanOf(new Add(EMPTY, ONE, TWO), THREE))); + Expression inn = isNotNull( + new And(EMPTY, new StartsWith(EMPTY, fieldA, pattern), greaterThanOf(new Add(EMPTY, ONE, TWO, TEST_CFG), THREE)) + ); Filter f = new Filter(EMPTY, relation, inn); Filter expected = new Filter(EMPTY, relation, new And(EMPTY, isNotNull(fieldA), inn)); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index fd5ff6b326f48..f0802fa096d15 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -174,6 +174,7 @@ import static org.elasticsearch.test.MapMatcher.assertMap; import static org.elasticsearch.xpack.esql.EsqlTestUtils.L; import static org.elasticsearch.xpack.esql.EsqlTestUtils.ONE; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_VERIFIER; import static org.elasticsearch.xpack.esql.EsqlTestUtils.THREE; import static org.elasticsearch.xpack.esql.EsqlTestUtils.TWO; @@ -5353,7 +5354,7 @@ record PushdownShadowingGeneratingPlanTestCase( // | EVAL y = to_integer(x), y = y + 1 new PushdownShadowingGeneratingPlanTestCase((plan, attr) -> { Alias y1 = new Alias(EMPTY, "y", new ToInteger(EMPTY, attr)); - Alias y2 = new Alias(EMPTY, "y", new Add(EMPTY, y1.toAttribute(), new Literal(EMPTY, 1, INTEGER))); + Alias y2 = new Alias(EMPTY, "y", new Add(EMPTY, y1.toAttribute(), new Literal(EMPTY, 1, INTEGER), TEST_CFG)); return new Eval(EMPTY, plan, List.of(y1, y2)); }, new PushDownEval()), // | DISSECT x "%{y} %{y}" diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanPreOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanPreOptimizerTests.java index 86582923b7af6..6f00cae5c0396 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanPreOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanPreOptimizerTests.java @@ -25,6 +25,7 @@ import java.util.List; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.esql.EsqlTestUtils.as; import static org.elasticsearch.xpack.esql.EsqlTestUtils.fieldAttribute; import static org.elasticsearch.xpack.esql.EsqlTestUtils.of; @@ -102,7 +103,7 @@ private Expression randomExpression() { return switch (randomInt(3)) { case 0 -> of(randomInt()); case 1 -> of(randomIdentifier()); - case 2 -> new Add(Source.EMPTY, of(randomInt()), of(randomDouble())); + case 2 -> new Add(Source.EMPTY, of(randomInt()), of(randomDouble()), TEST_CFG); default -> new Concat(Source.EMPTY, of(randomIdentifier()), randomList(1, 10, () -> of(randomIdentifier()))); }; } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/OptimizerRulesTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/OptimizerRulesTests.java index ab15014270bfa..3248ffb4167d1 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/OptimizerRulesTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/OptimizerRulesTests.java @@ -8,6 +8,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.esql.capabilities.ConfigurationAware; import org.elasticsearch.xpack.esql.core.expression.Alias; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; @@ -135,7 +136,7 @@ protected Expression rule(Expression e, LogicalOptimizerContext ctx) { var literal = new Literal(new Source(1, 25, "1"), 1, DataType.INTEGER); var attribute = new UnresolvedAttribute(new Source(1, 20, "f1"), "f1"); - var add = new Add(new Source(1, 20, "f1+1"), attribute, literal); + var add = new Add(new Source(1, 20, "f1+1"), attribute, literal, ConfigurationAware.CONFIGURATION_MARKER); var alias = new Alias(new Source(1, 18, "x=f1+1"), "x", add); // contains expressions only from EVAL diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ConstantFoldingTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ConstantFoldingTests.java index f7ea5d17f485b..8f32cc52d0d8b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ConstantFoldingTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/ConstantFoldingTests.java @@ -33,6 +33,7 @@ import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import static org.elasticsearch.xpack.esql.EsqlTestUtils.FIVE; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.esql.EsqlTestUtils.THREE; import static org.elasticsearch.xpack.esql.EsqlTestUtils.TWO; import static org.elasticsearch.xpack.esql.EsqlTestUtils.as; @@ -58,7 +59,7 @@ private Expression constantFolding(Expression e) { } public void testConstantFolding() { - Expression exp = new Add(EMPTY, TWO, THREE); + Expression exp = new Add(EMPTY, TWO, THREE, TEST_CFG); assertTrue(exp.foldable()); Expression result = constantFolding(exp); @@ -120,8 +121,8 @@ public void testConstantFoldingLikes() { } public void testArithmeticFolding() { - assertEquals(10, foldOperator(new Add(EMPTY, new Literal(EMPTY, 7, DataType.INTEGER), THREE))); - assertEquals(4, foldOperator(new Sub(EMPTY, new Literal(EMPTY, 7, DataType.INTEGER), THREE))); + assertEquals(10, foldOperator(new Add(EMPTY, new Literal(EMPTY, 7, DataType.INTEGER), THREE, TEST_CFG))); + assertEquals(4, foldOperator(new Sub(EMPTY, new Literal(EMPTY, 7, DataType.INTEGER), THREE, TEST_CFG))); assertEquals(21, foldOperator(new Mul(EMPTY, new Literal(EMPTY, 7, DataType.INTEGER), THREE))); assertEquals(2, foldOperator(new Div(EMPTY, new Literal(EMPTY, 7, DataType.INTEGER), THREE))); assertEquals(1, foldOperator(new Mod(EMPTY, new Literal(EMPTY, 7, DataType.INTEGER), THREE))); @@ -130,8 +131,8 @@ public void testArithmeticFolding() { public void testFoldRange() { // 1 + 9 < value AND value < 20-1 // with value = 12 and randomly replacing the `<` by `<=` - Expression lowerBound = new Add(EMPTY, new Literal(EMPTY, 1, DataType.INTEGER), new Literal(EMPTY, 9, DataType.INTEGER)); - Expression upperBound = new Sub(EMPTY, new Literal(EMPTY, 20, DataType.INTEGER), new Literal(EMPTY, 1, DataType.INTEGER)); + Expression lowerBound = new Add(EMPTY, new Literal(EMPTY, 1, DataType.INTEGER), new Literal(EMPTY, 9, DataType.INTEGER), TEST_CFG); + Expression upperBound = new Sub(EMPTY, new Literal(EMPTY, 20, DataType.INTEGER), new Literal(EMPTY, 1, DataType.INTEGER), TEST_CFG); Expression value = new Literal(EMPTY, 12, DataType.INTEGER); Range range = new Range(EMPTY, value, lowerBound, randomBoolean(), upperBound, randomBoolean(), randomZone()); @@ -146,17 +147,17 @@ public void testFoldRangeWithInvalidBoundaries() { boolean includeLowerBound = randomBoolean(); if (includeLowerBound) { // 1 + 10 <= value - lowerBound = new Add(EMPTY, new Literal(EMPTY, 1, DataType.INTEGER), new Literal(EMPTY, 10, DataType.INTEGER)); + lowerBound = new Add(EMPTY, new Literal(EMPTY, 1, DataType.INTEGER), new Literal(EMPTY, 10, DataType.INTEGER), TEST_CFG); } else { // 1 + 9 < value - lowerBound = new Add(EMPTY, new Literal(EMPTY, 1, DataType.INTEGER), new Literal(EMPTY, 9, DataType.INTEGER)); + lowerBound = new Add(EMPTY, new Literal(EMPTY, 1, DataType.INTEGER), new Literal(EMPTY, 9, DataType.INTEGER), TEST_CFG); } boolean includeUpperBound = randomBoolean(); // value < 11 - 1 // or // value <= 11 - 1 - Expression upperBound = new Sub(EMPTY, new Literal(EMPTY, 11, DataType.INTEGER), new Literal(EMPTY, 1, DataType.INTEGER)); + Expression upperBound = new Sub(EMPTY, new Literal(EMPTY, 11, DataType.INTEGER), new Literal(EMPTY, 1, DataType.INTEGER), TEST_CFG); Expression value = fieldAttribute(); 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 733eb049b88d7..0f247a1308c73 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 @@ -95,7 +95,7 @@ private Expression foldNull(Expression e) { } public void testBasicNullFolding() { - assertNullLiteral(foldNull(new Add(EMPTY, L(randomInt()), NULL))); + assertNullLiteral(foldNull(new Add(EMPTY, L(randomInt()), NULL, TEST_CFG))); assertNullLiteral(foldNull(new Round(EMPTY, NULL, null))); assertNullLiteral(foldNull(new Pow(EMPTY, NULL, NULL))); assertNullLiteral(foldNull(new DateFormat(EMPTY, NULL, NULL, TEST_CFG))); @@ -149,7 +149,7 @@ public void testNullFoldingIsNull() { public void testGenericNullableExpression() { FoldNull rule = new FoldNull(); // arithmetic - assertNullLiteral(foldNull(new Add(EMPTY, getFieldAttribute("a"), NULL))); + assertNullLiteral(foldNull(new Add(EMPTY, getFieldAttribute("a"), NULL, TEST_CFG))); // comparison assertNullLiteral(foldNull(greaterThanOf(getFieldAttribute("a"), NULL))); // regex @@ -232,10 +232,10 @@ public void testNullFoldableDoesNotApplyToIsNullAndNotNull() { DataType numericType = randomFrom(INTEGER, LONG, DOUBLE); DataType genericType = randomFrom(INTEGER, LONG, DOUBLE, UNSIGNED_LONG, KEYWORD, TEXT, GEO_POINT, GEO_SHAPE, VERSION, IP); List items = List.of( - new Add(EMPTY, getFieldAttribute("a", numericType), getFieldAttribute("b", numericType)), - new Add(EMPTY, new Literal(EMPTY, 1, INTEGER), new Literal(EMPTY, List.of(1, 2, 3), INTEGER)), - new Sub(EMPTY, getFieldAttribute("a", numericType), getFieldAttribute("b", numericType)), - new Sub(EMPTY, new Literal(EMPTY, 1, INTEGER), new Literal(EMPTY, List.of(1, 2, 3), INTEGER)), + new Add(EMPTY, getFieldAttribute("a", numericType), getFieldAttribute("b", numericType), TEST_CFG), + new Add(EMPTY, new Literal(EMPTY, 1, INTEGER), new Literal(EMPTY, List.of(1, 2, 3), INTEGER), TEST_CFG), + new Sub(EMPTY, getFieldAttribute("a", numericType), getFieldAttribute("b", numericType), TEST_CFG), + new Sub(EMPTY, new Literal(EMPTY, 1, INTEGER), new Literal(EMPTY, List.of(1, 2, 3), INTEGER), TEST_CFG), new Mul(EMPTY, getFieldAttribute("a", numericType), getFieldAttribute("b", numericType)), new Mul(EMPTY, new Literal(EMPTY, 1, INTEGER), new Literal(EMPTY, List.of(1, 2, 3), INTEGER)), new Div(EMPTY, getFieldAttribute("a", numericType), getFieldAttribute("b", numericType)), diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PropagateNullableTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PropagateNullableTests.java index 479b5072864f0..c805ef45f9d45 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PropagateNullableTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PropagateNullableTests.java @@ -25,6 +25,7 @@ import static java.util.Arrays.asList; import static org.elasticsearch.xpack.esql.EsqlTestUtils.ONE; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.esql.EsqlTestUtils.THREE; import static org.elasticsearch.xpack.esql.EsqlTestUtils.TWO; import static org.elasticsearch.xpack.esql.EsqlTestUtils.getFieldAttribute; @@ -100,8 +101,8 @@ public void testIsNullAndDeeplyNestedExpression() { Expression nullified = new And( EMPTY, - greaterThanOf(new Div(EMPTY, new Add(EMPTY, fa, ONE), TWO), ONE), - greaterThanOf(new Add(EMPTY, fa, TWO), ONE) + greaterThanOf(new Div(EMPTY, new Add(EMPTY, fa, ONE, TEST_CFG), TWO), ONE), + greaterThanOf(new Add(EMPTY, fa, TWO, TEST_CFG), ONE) ); Expression kept = new And(EMPTY, isNull, lessThanOf(getFieldAttribute("b"), THREE)); And and = new And(EMPTY, nullified, kept); @@ -136,7 +137,7 @@ public void testIsNullDisjunction() { IsNull isNull = new IsNull(EMPTY, fa); Or or = new Or(EMPTY, isNull, greaterThanOf(fa, THREE)); - And and = new And(EMPTY, new Add(EMPTY, fa, ONE), or); + And and = new And(EMPTY, new Add(EMPTY, fa, ONE, TEST_CFG), or); assertEquals(and, propagateNullable(and)); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/PushTopNToSourceTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/PushTopNToSourceTests.java index 3ac1e5e15ec5f..69123389f6105 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/PushTopNToSourceTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/physical/local/PushTopNToSourceTests.java @@ -48,6 +48,7 @@ import java.util.Map; import java.util.function.Function; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_PLANNER_SETTINGS; import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE; import static org.elasticsearch.xpack.esql.core.type.DataType.GEO_POINT; @@ -647,7 +648,7 @@ Expression k(String value) { } public Expression add(Expression left, Expression right) { - return new Add(Source.EMPTY, left, right); + return new Add(Source.EMPTY, left, right, TEST_CFG); } public Expression distance(String left, String right) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/ExpressionTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/ExpressionTests.java index eca519024cee3..0fe64af77a904 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/ExpressionTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/ExpressionTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.esql.capabilities.ConfigurationAware; import org.elasticsearch.xpack.esql.core.expression.Alias; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.FoldContext; @@ -360,7 +361,12 @@ public void testFunctionExpressions() { new ArrayList<>( List.of( new UnresolvedAttribute(EMPTY, "a"), - new Add(EMPTY, new UnresolvedAttribute(EMPTY, "b"), new UnresolvedAttribute(EMPTY, "c")) + new Add( + EMPTY, + new UnresolvedAttribute(EMPTY, "b"), + new UnresolvedAttribute(EMPTY, "c"), + ConfigurationAware.CONFIGURATION_MARKER + ) ) ) ), diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java index 483183fb6a0d5..ff9c4d8d082b5 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.json.JsonXContent; import org.elasticsearch.xpack.esql.action.EsqlCapabilities; +import org.elasticsearch.xpack.esql.capabilities.ConfigurationAware; import org.elasticsearch.xpack.esql.core.capabilities.UnresolvedException; import org.elasticsearch.xpack.esql.core.expression.Alias; import org.elasticsearch.xpack.esql.core.expression.EmptyAttribute; @@ -296,7 +297,10 @@ public void testEval() { new Eval( EMPTY, PROCESSING_CMD_INPUT, - List.of(new Alias(EMPTY, "b", attribute("a")), new Alias(EMPTY, "c", new Add(EMPTY, attribute("a"), integer(1)))) + List.of( + new Alias(EMPTY, "b", attribute("a")), + new Alias(EMPTY, "c", new Add(EMPTY, attribute("a"), integer(1), ConfigurationAware.CONFIGURATION_MARKER)) + ) ), processingCommand("eval b = a, c = a + 1") ); @@ -316,7 +320,12 @@ public void testEvalImplicitNames() { new Alias( EMPTY, "fn(a + 1)", - new UnresolvedFunction(EMPTY, "fn", DEFAULT, List.of(new Add(EMPTY, attribute("a"), integer(1)))) + new UnresolvedFunction( + EMPTY, + "fn", + DEFAULT, + List.of(new Add(EMPTY, attribute("a"), integer(1), ConfigurationAware.CONFIGURATION_MARKER)) + ) ) ) ), diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/QueryPlanTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/QueryPlanTests.java index dadcd12b31030..a0ce6edcd405b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/QueryPlanTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/QueryPlanTests.java @@ -27,6 +27,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.TEST_CFG; import static org.elasticsearch.xpack.esql.EsqlTestUtils.equalsOf; import static org.elasticsearch.xpack.esql.EsqlTestUtils.fieldAttribute; import static org.elasticsearch.xpack.esql.EsqlTestUtils.of; @@ -154,7 +155,7 @@ public void testPlanExpressions() { public void testPlanReferences() { var one = fieldAttribute("one", INTEGER); var two = fieldAttribute("two", INTEGER); - var add = new Add(EMPTY, one, two); + var add = new Add(EMPTY, one, two, TEST_CFG); var field = fieldAttribute("field", INTEGER); var filter = new Filter(EMPTY, relation(), equalsOf(field, add)); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/EvalExecSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/EvalExecSerializationTests.java index 45baf4822b1d2..54a24cbc6d73b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/EvalExecSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/EvalExecSerializationTests.java @@ -17,22 +17,23 @@ import java.util.List; public class EvalExecSerializationTests extends AbstractPhysicalPlanSerializationTests { - public static EvalExec randomEvalExec(int depth) { + public EvalExec randomEvalExec(int depth) { Source source = randomSource(); PhysicalPlan child = randomChild(depth); List fields = randomFields(); return new EvalExec(source, child, fields); } - public static List randomFields() { - return randomList(1, 10, EvalExecSerializationTests::randomField); + public List randomFields() { + return randomList(1, 10, this::randomField); } - public static Alias randomField() { + public Alias randomField() { Expression child = new Add( randomSource(), FieldAttributeTests.createFieldAttribute(0, true), - FieldAttributeTests.createFieldAttribute(0, true) + FieldAttributeTests.createFieldAttribute(0, true), + configuration() ); return new Alias(randomSource(), randomAlphaOfLength(5), child); } @@ -49,7 +50,7 @@ protected EvalExec mutateInstance(EvalExec instance) throws IOException { if (randomBoolean()) { child = randomValueOtherThan(child, () -> randomChild(0)); } else { - fields = randomValueOtherThan(fields, EvalExecSerializationTests::randomFields); + fields = randomValueOtherThan(fields, this::randomFields); } return new EvalExec(instance.source(), child, fields); } 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 b60a9867630d6..f327da2332e34 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 @@ -94,8 +94,8 @@ public static List params() { List params = new ArrayList<>(); for (Expression e : new Expression[] { - new Add(Source.EMPTY, DOUBLE1, DOUBLE2), - new Sub(Source.EMPTY, DOUBLE1, DOUBLE2), + new Add(Source.EMPTY, DOUBLE1, DOUBLE2, TEST_CONFIG), + new Sub(Source.EMPTY, DOUBLE1, DOUBLE2, TEST_CONFIG), new Mul(Source.EMPTY, DOUBLE1, DOUBLE2), new Div(Source.EMPTY, DOUBLE1, DOUBLE2), new Mod(Source.EMPTY, DOUBLE1, DOUBLE2), diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/DataTypeConversionTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/DataTypeConversionTests.java index 16935afcc0ef6..285a434262109 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/DataTypeConversionTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/DataTypeConversionTests.java @@ -15,10 +15,12 @@ import org.elasticsearch.xpack.esql.core.type.Converter; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.core.type.DataTypeConverter; +import org.elasticsearch.xpack.esql.core.util.DateUtils; import org.elasticsearch.xpack.versionfield.Version; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import static org.elasticsearch.xpack.esql.core.type.DataType.BOOLEAN; @@ -572,4 +574,12 @@ public void testVersionToString() { versionToString.convert(stringToVersion.convert(new Literal(s2, BytesRefs.toBytesRef("2.1.4-SNAPSHOT"), stringType))) ); } + + private ZonedDateTime asDateTime(long millis) { + return DateUtils.asDateTime(millis, ZoneOffset.UTC); + } + + private ZonedDateTime asDateTime(String dateString) { + return DateUtils.asDateTime(dateString); + } }