diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/promql/predicate/operator/VectorBinaryOperator.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/promql/predicate/operator/VectorBinaryOperator.java index 06075466a5e79..81e0ee6a3541e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/promql/predicate/operator/VectorBinaryOperator.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/promql/predicate/operator/VectorBinaryOperator.java @@ -14,8 +14,6 @@ import org.elasticsearch.xpack.esql.core.expression.function.Function; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; -import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper; -import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction; 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; @@ -163,9 +161,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; if (super.equals(o)) { VectorBinaryOperator that = (VectorBinaryOperator) o; - return dropMetricName == that.dropMetricName - && Objects.equals(match, that.match) - && Objects.equals(binaryOp, that.binaryOp); + return dropMetricName == that.dropMetricName && Objects.equals(match, that.match) && Objects.equals(binaryOp, that.binaryOp); } return false; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/promql/predicate/operator/comparison/VectorBinaryComparison.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/promql/predicate/operator/comparison/VectorBinaryComparison.java index f34eaa9e7ac91..d42960a30f6b3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/promql/predicate/operator/comparison/VectorBinaryComparison.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/promql/predicate/operator/comparison/VectorBinaryComparison.java @@ -47,7 +47,14 @@ public ScalarFunctionFactory asFunction() { private final ComparisonOp op; private final boolean boolMode; - public VectorBinaryComparison(Source source, LogicalPlan left, LogicalPlan right, VectorMatch match, boolean boolMode, ComparisonOp op) { + public VectorBinaryComparison( + Source source, + LogicalPlan left, + LogicalPlan right, + VectorMatch match, + boolean boolMode, + ComparisonOp op + ) { super(source, left, right, match, boolMode == false, op); this.op = op; this.boolMode = boolMode; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java index eb47c185ac488..30d344bbb3f2b 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java @@ -88,13 +88,17 @@ 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.promql.PromqlCommand; +import org.elasticsearch.xpack.esql.plan.logical.promql.PromqlParams; import org.elasticsearch.xpack.esql.plan.logical.show.ShowInfo; import org.joni.exception.SyntaxException; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -117,6 +121,9 @@ */ public class LogicalPlanBuilder extends ExpressionBuilder { + private static final String TIME = "time", START = "start", END = "end", STEP = "step"; + private static final Set PROMQL_ALLOWED_PARAMS = Set.of(TIME, START, END, STEP); + /** * Maximum number of commands allowed per query */ @@ -1219,52 +1226,7 @@ public PlanFactory visitSampleCommand(EsqlBaseParser.SampleCommandContext ctx) { @Override public PlanFactory visitPromqlCommand(EsqlBaseParser.PromqlCommandContext ctx) { Source source = source(ctx); - Map params = new HashMap<>(); - String TIME = "time", START = "start", END = "end", STEP = "step"; - Set allowed = Set.of(TIME, START, END, STEP); - - if (ctx.promqlParam().isEmpty()) { - throw new ParsingException(source(ctx), "Parameter [{}] or [{}] is required", STEP, TIME); - } - - for (EsqlBaseParser.PromqlParamContext paramCtx : ctx.promqlParam()) { - var paramNameCtx = paramCtx.name; - String name = paramNameCtx.getText(); - if (params.containsKey(name)) { - throw new ParsingException(source(paramNameCtx), "[{}] already specified", name); - } - if (allowed.contains(name) == false) { - String message = "Unknown parameter [{}]"; - List similar = StringUtils.findSimilar(name, allowed); - if (CollectionUtils.isEmpty(similar) == false) { - message += ", did you mean " + (similar.size() == 1 ? "[" + similar.get(0) + "]" : "any of " + similar) + "?"; - } - throw new ParsingException(source(paramNameCtx), message, name); - } - String value = paramCtx.value.getText(); - // TODO: validate and convert the value - - } - - // Validation logic for time parameters - Expression time = params.get(TIME); - Expression start = params.get(START); - Expression end = params.get(END); - Expression step = params.get(STEP); - - if (time != null && (start != null || end != null || step != null)) { - throw new ParsingException( - source, - "Specify either [{}] for instant query or [{}}], [{}] or [{}}] for a range query", - TIME, - STEP, - START, - END - ); - } - if ((start != null || end != null) && step == null) { - throw new ParsingException(source, "[{}}] is required alongside [{}}] or [{}}]", STEP, START, END); - } + PromqlParams params = parsePromqlParams(ctx, source); // TODO: Perform type and value validation var queryCtx = ctx.promqlQueryPart(); @@ -1292,9 +1254,95 @@ public PlanFactory visitPromqlCommand(EsqlBaseParser.PromqlCommandContext ctx) { throw PromqlParserUtils.adjustParsingException(pe, promqlStartLine, promqlStartColumn); } - return plan -> time != null - ? new PromqlCommand(source, plan, promqlPlan, time) - : new PromqlCommand(source, plan, promqlPlan, start, end, step); + return plan -> new PromqlCommand(source, plan, promqlPlan, params); } + private static PromqlParams parsePromqlParams(EsqlBaseParser.PromqlCommandContext ctx, Source source) { + Instant time = null; + Instant start = null; + Instant end = null; + Duration step = null; + + Set paramsSeen = new HashSet<>(); + for (EsqlBaseParser.PromqlParamContext paramCtx : ctx.promqlParam()) { + String name = param(paramCtx.name); + if (paramsSeen.add(name) == false) { + throw new ParsingException(source(paramCtx.name), "[{}] already specified", name); + } + Source valueSource = source(paramCtx.value); + String valueString = param(paramCtx.value); + switch (name) { + case TIME -> time = PromqlParserUtils.parseDate(valueSource, valueString); + case START -> start = PromqlParserUtils.parseDate(valueSource, valueString); + case END -> end = PromqlParserUtils.parseDate(valueSource, valueString); + case STEP -> { + try { + step = Duration.ofSeconds(Integer.parseInt(valueString)); + } catch (NumberFormatException ignore) { + step = PromqlParserUtils.parseDuration(valueSource, valueString); + } + } + default -> { + String message = "Unknown parameter [{}]"; + List similar = StringUtils.findSimilar(name, PROMQL_ALLOWED_PARAMS); + if (CollectionUtils.isEmpty(similar) == false) { + message += ", did you mean " + (similar.size() == 1 ? "[" + similar.get(0) + "]" : "any of " + similar) + "?"; + } + throw new ParsingException(source(paramCtx.name), message, name); + } + } + } + + // Validation logic for time parameters + if (time != null) { + if (start != null || end != null || step != null) { + throw new ParsingException( + source, + "Specify either [{}] for instant query or [{}}], [{}] or [{}}] for a range query", + TIME, + STEP, + START, + END + ); + } + } else if (step != null) { + if (start != null || end != null) { + if (start == null || end == null) { + throw new ParsingException( + source, + "Parameters [{}] and [{}] must either both be specified or both be omitted for a range query", + START, + END + ); + } + if (end.isBefore(start)) { + throw new ParsingException( + source, + "invalid parameter \"end\": end timestamp must not be before start time", + end, + start + ); + } + } + if (step.isPositive() == false) { + throw new ParsingException( + source, + "invalid parameter \"step\": zero or negative query resolution step widths are not accepted. " + + "Try a positive integer", + step + ); + } + } else { + throw new ParsingException(source, "Parameter [{}] or [{}] is required", STEP, TIME); + } + return new PromqlParams(time, start, end, step); + } + + private static String param(EsqlBaseParser.PromqlParamContentContext paramCtx) { + if (paramCtx.QUOTED_IDENTIFIER() != null) { + return AbstractBuilder.unquote(paramCtx.QUOTED_IDENTIFIER().getText()); + } else { + return paramCtx.getText(); + } + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlExpressionBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlExpressionBuilder.java index 93a88889087c1..b78ae4a7a0f81 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlExpressionBuilder.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlExpressionBuilder.java @@ -173,10 +173,7 @@ public Duration visitDuration(DurationContext ctx) { } // Non-literal LogicalPlan - throw new ParsingException( - source(ctx), - "Duration must be a constant expression" - ); + throw new ParsingException(source(ctx), "Duration must be a constant expression"); } case Expression e -> { // Fallback for Expression (shouldn't happen with new logic) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlFoldingUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlFoldingUtils.java index 5071fae0517d2..bb0213b40cb77 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlFoldingUtils.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlFoldingUtils.java @@ -64,11 +64,7 @@ private static Duration arithmetics(Source source, Duration left, Duration right Duration result = switch (op) { case ADD -> left.plus(right); case SUB -> left.minus(right); - default -> throw new ParsingException( - source, - "Operation [{}] not supported between two durations", - op - ); + default -> throw new ParsingException(source, "Operation [{}] not supported between two durations", op); }; return result; @@ -120,11 +116,7 @@ private static Duration arithmetics(Source source, Number scalar, Duration durat case ADD -> arithmetics(source, duration, scalar, ArithmeticOp.ADD); case SUB -> arithmetics(source, Duration.ofSeconds(scalar.longValue()), duration, ArithmeticOp.SUB); case MUL -> arithmetics(source, duration, scalar, ArithmeticOp.MUL); - default -> throw new ParsingException( - source, - "Operation [{}] not supported with scalar on left and duration on right", - op - ); + default -> throw new ParsingException(source, "Operation [{}] not supported with scalar on left and duration on right", op); }; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlLogicalPlanBuilder.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlLogicalPlanBuilder.java index 138678f8a31f4..194b0d2848dda 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlLogicalPlanBuilder.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlLogicalPlanBuilder.java @@ -28,7 +28,6 @@ import org.elasticsearch.xpack.esql.expression.promql.predicate.operator.comparison.VectorBinaryComparison.ComparisonOp; import org.elasticsearch.xpack.esql.expression.promql.predicate.operator.set.VectorBinarySet; import org.elasticsearch.xpack.esql.expression.promql.subquery.Subquery; -import org.elasticsearch.xpack.esql.parser.EsqlBaseParser; import org.elasticsearch.xpack.esql.parser.ParsingException; import org.elasticsearch.xpack.esql.parser.PromqlBaseParser; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; @@ -296,7 +295,7 @@ public LogicalPlan visitArithmeticBinary(PromqlBaseParser.ArithmeticBinaryContex Object leftValue = leftLiteral.value(); Object rightValue = rightLiteral.value(); - // arithmetics + // arithmetics if (binaryOperator instanceof ArithmeticOp arithmeticOp) { Object result = PromqlFoldingUtils.evaluate(source, leftValue, rightValue, arithmeticOp); DataType resultType = determineResultType(result); @@ -356,20 +355,12 @@ public LogicalPlan visitArithmeticBinary(PromqlBaseParser.ArithmeticBinaryContex return switch (binaryOperator) { case ArithmeticOp arithmeticOp -> new VectorBinaryArithmetic(source, le, re, modifier, arithmeticOp); - case ComparisonOp comparisonOp -> new VectorBinaryComparison( - source, - le, - re, - modifier, - bool, - comparisonOp - ); + case ComparisonOp comparisonOp -> new VectorBinaryComparison(source, le, re, modifier, bool, comparisonOp); case VectorBinarySet.SetOp setOp -> new VectorBinarySet(source, le, re, modifier, setOp); default -> throw new ParsingException(source(ctx.op), "Unknown arithmetic {}", opText); }; } - private BinaryOp binaryOp(Token opType) { return switch (opType.getType()) { case CARET -> ArithmeticOp.POW; @@ -548,8 +539,7 @@ public Duration visitSubqueryResolution(PromqlBaseParser.SubqueryResolutionConte return duration; } - throw new ParsingException(source(ctx), "Expected duration result, got [{}]", - result.getClass().getSimpleName()); + throw new ParsingException(source(ctx), "Expected duration result, got [{}]", result.getClass().getSimpleName()); } // Just COLON with no resolution - use default diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlParserUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlParserUtils.java index 1a944137b6b89..6f38ed51cc598 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlParserUtils.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlParserUtils.java @@ -13,6 +13,8 @@ import org.elasticsearch.xpack.esql.parser.ParsingException; import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeParseException; import java.util.LinkedHashMap; import java.util.Map; @@ -272,4 +274,20 @@ private static int adjustColumn(int lineNumber, int columnNumber, int startColum // the column offset only applies to the first line of the PROMQL command return lineNumber == 1 ? columnNumber + startColumn - 1 : columnNumber; } + + /* + * Parses a Prometheus date which can be either a float representing epoch seconds or an RFC3339 date string. + */ + public static Instant parseDate(Source source, String value) { + try { + return Instant.ofEpochMilli((long) (Double.parseDouble(value) * 1000)); + } catch (NumberFormatException ignore) { + // Not a float, try parsing as date string + } + try { + return Instant.parse(value); + } catch (DateTimeParseException e) { + throw new ParsingException(source, "Invalid date format [{}]", value); + } + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/PromqlCommand.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/PromqlCommand.java index 81f612ae5f53e..cdf8459bbeb02 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/PromqlCommand.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/PromqlCommand.java @@ -8,12 +8,9 @@ package org.elasticsearch.xpack.esql.plan.logical.promql; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.core.TimeValue; import org.elasticsearch.xpack.esql.capabilities.PostAnalysisVerificationAware; import org.elasticsearch.xpack.esql.capabilities.TelemetryAware; import org.elasticsearch.xpack.esql.common.Failures; -import org.elasticsearch.xpack.esql.core.expression.Expression; -import org.elasticsearch.xpack.esql.core.expression.Literal; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; @@ -22,6 +19,7 @@ import java.io.IOException; import java.time.Duration; +import java.time.Instant; import java.util.Objects; import static org.elasticsearch.xpack.esql.common.Failure.fail; @@ -33,34 +31,27 @@ public class PromqlCommand extends UnaryPlan implements TelemetryAware, PostAnalysisVerificationAware { private final LogicalPlan promqlPlan; - private final Expression start, end, step; - - // Instant query constructor - shortcut for a range constructor - public PromqlCommand(Source source, LogicalPlan child, LogicalPlan promqlPlan, Expression time) { - this(source, child, promqlPlan, time, time, Literal.timeDuration(source, Duration.ZERO)); - } + private final PromqlParams params; // Range query constructor - public PromqlCommand(Source source, LogicalPlan child, LogicalPlan promqlPlan, Expression start, Expression end, Expression step) { + public PromqlCommand(Source source, LogicalPlan child, LogicalPlan promqlPlan, PromqlParams params) { super(source, child); this.promqlPlan = promqlPlan; - this.start = start; - this.end = end; - this.step = step; + this.params = params; } @Override protected NodeInfo info() { - return NodeInfo.create(this, PromqlCommand::new, child(), promqlPlan(), start(), end(), step()); + return NodeInfo.create(this, PromqlCommand::new, child(), promqlPlan(), params()); } @Override public PromqlCommand replaceChild(LogicalPlan newChild) { - return new PromqlCommand(source(), newChild, promqlPlan(), start(), end(), step()); + return new PromqlCommand(source(), newChild, promqlPlan(), params()); } public PromqlCommand withPromqlPlan(LogicalPlan newPromqlPlan) { - return new PromqlCommand(source(), child(), newPromqlPlan, start(), end(), step()); + return new PromqlCommand(source(), child(), newPromqlPlan, params()); } @Override @@ -87,21 +78,37 @@ public LogicalPlan promqlPlan() { return promqlPlan; } - public Expression start() { - return start; + public PromqlParams params() { + return params; + } + + public Instant start() { + return params().start(); + } + + public Instant end() { + return params().end(); } - public Expression end() { - return end; + public Instant time() { + return params().time(); } - public Expression step() { - return step; + public Duration step() { + return params().step(); + } + + public boolean isInstantQuery() { + return params().isInstantQuery(); + } + + public boolean isRangeQuery() { + return params().isRangeQuery(); } @Override public int hashCode() { - return Objects.hash(child(), start, end, step, promqlPlan); + return Objects.hash(child(), params, promqlPlan); } @Override @@ -119,11 +126,8 @@ public boolean equals(Object obj) { public String nodeString() { StringBuilder sb = new StringBuilder(); sb.append(nodeName()); - if (start == end) { - sb.append("time=").append(start); - } else { - sb.append("start=").append(start).append(", end=").append(end).append(", step=").append(step); - } + sb.append("params="); + sb.append(params.toString()); sb.append(" promql=[<>\n"); sb.append(promqlPlan.toString()); sb.append("\n<>]]"); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/PromqlParams.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/PromqlParams.java new file mode 100644 index 0000000000000..e09852d840b57 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/PromqlParams.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.plan.logical.promql; + +import java.time.Duration; +import java.time.Instant; + +/** + * Container for PromQL command parameters: + *
    + *
  • time for instant queries
  • + *
  • start, end, step for range queries
  • + *
+ * These can be specified in the {@linkplain org.elasticsearch.xpack.esql.plan.logical.promql.PromqlCommand PROMQL command} like so: + *
+ *     # instant query
+ *     PROMQL time `2025-10-31T00:00:00Z` (avg(foo))
+ *     # range query with explicit start and end
+ *     PROMQL start `2025-10-31T00:00:00Z` end `2025-10-31T01:00:00Z` step 1m (avg(foo))
+ *     # range query with implicit time bounds, doesn't support calling {@code start()} or {@code end()} functions
+ *     PROMQL step 5m (avg(foo))
+ * 
+ * + * @see PromQL API documentation + */ +public record PromqlParams(Instant time, Instant start, Instant end, Duration step) { + + public boolean isInstantQuery() { + return time != null; + } + + public boolean isRangeQuery() { + return step != null; + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/selector/Selector.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/selector/Selector.java index d1648292756de..ea9ada7de1d4c 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/selector/Selector.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/selector/Selector.java @@ -18,7 +18,6 @@ import org.elasticsearch.xpack.esql.plan.logical.promql.PlaceholderRelation; import java.io.IOException; -import java.sql.Time; import java.util.List; import java.util.Objects; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/promql/PromqlVerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/promql/PromqlVerifierTests.java index 507c3d20ebc63..77ca26502939c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/promql/PromqlVerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/promql/PromqlVerifierTests.java @@ -28,9 +28,7 @@ public void testPromqlMissingAcrossSeriesAggregation() { TS test | PROMQL step 5m ( rate(network.bytes_in[5m]) )""", tsdb), - equalTo( - "2:3: only aggregations across timeseries are supported at this time (found [rate(network.bytes_in[5m])])" - ) + equalTo("2:3: only aggregations across timeseries are supported at this time (found [rate(network.bytes_in[5m])])") ); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/promql/PromqlLogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/promql/PromqlLogicalPlanOptimizerTests.java index 38b75599a3f3f..528e7fa02363a 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/promql/PromqlLogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/promql/PromqlLogicalPlanOptimizerTests.java @@ -13,8 +13,8 @@ import org.elasticsearch.xpack.esql.action.EsqlCapabilities; import org.elasticsearch.xpack.esql.analysis.Analyzer; import org.elasticsearch.xpack.esql.analysis.AnalyzerContext; -import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RegexMatch; +import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry; import org.elasticsearch.xpack.esql.expression.function.scalar.string.StartsWith; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In; @@ -280,26 +280,26 @@ public void testGrammar() { System.out.println(plan); } -// public void testPromqlArithmetricOperators() { -// // TODO doesn't parse -// // line 1:27: Invalid query '1+1'[ArithmeticBinaryContext] given; expected LogicalPlan but found VectorBinaryArithmetic -// assertThat( -// error("TS test | PROMQL step 5m (1+1)", tsdb), -// equalTo("1:1: arithmetic operators are not supported at this time [foo]") -// ); -// assertThat( -// error("TS test | PROMQL step 5m ( foo and bar )", tsdb), -// equalTo("1:1: arithmetic operators are not supported at this time [foo]") -// ); -// assertThat( -// error("TS test | PROMQL step 5m (1+foo)", tsdb), -// equalTo("1:1: arithmetic operators are not supported at this time [foo]") -// ); -// assertThat( -// error("TS test | PROMQL step 5m (foo+bar)", tsdb), -// equalTo("1:1: arithmetic operators are not supported at this time [foo]") -// ); -// } + // public void testPromqlArithmetricOperators() { + // // TODO doesn't parse + // // line 1:27: Invalid query '1+1'[ArithmeticBinaryContext] given; expected LogicalPlan but found VectorBinaryArithmetic + // assertThat( + // error("TS test | PROMQL step 5m (1+1)", tsdb), + // equalTo("1:1: arithmetic operators are not supported at this time [foo]") + // ); + // assertThat( + // error("TS test | PROMQL step 5m ( foo and bar )", tsdb), + // equalTo("1:1: arithmetic operators are not supported at this time [foo]") + // ); + // assertThat( + // error("TS test | PROMQL step 5m (1+foo)", tsdb), + // equalTo("1:1: arithmetic operators are not supported at this time [foo]") + // ); + // assertThat( + // error("TS test | PROMQL step 5m (foo+bar)", tsdb), + // equalTo("1:1: arithmetic operators are not supported at this time [foo]") + // ); + // } protected LogicalPlan planPromql(String query) { var analyzed = tsAnalyzer.analyze(parser.createStatement(query)); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/PromqlAstTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/PromqlAstTests.java index d07938aa8aad5..6feb65eeef201 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/PromqlAstTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/PromqlAstTests.java @@ -7,12 +7,10 @@ package org.elasticsearch.xpack.esql.parser.promql; -import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.core.Tuple; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.core.QlClientException; import org.elasticsearch.xpack.esql.parser.ParsingException; @@ -32,7 +30,7 @@ * Test for checking the overall grammar by throwing a number of valid queries at the parser to see whether any exception is raised. * In time, the queries themselves get to be checked against the actual execution model and eventually against the expected results. */ -//@TestLogging(reason = "debug", value = "org.elasticsearch.xpack.esql.parser.promql:TRACE") +// @TestLogging(reason = "debug", value = "org.elasticsearch.xpack.esql.parser.promql:TRACE") public class PromqlAstTests extends ESTestCase { private static final Logger log = LogManager.getLogger(PromqlAstTests.class); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/PromqlParamsTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/PromqlParamsTests.java new file mode 100644 index 0000000000000..5e4509fc495ef --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/PromqlParamsTests.java @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.parser.promql; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.esql.parser.EsqlParser; +import org.elasticsearch.xpack.esql.parser.ParsingException; +import org.elasticsearch.xpack.esql.plan.logical.promql.PromqlCommand; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +import static org.elasticsearch.xpack.esql.EsqlTestUtils.as; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +public class PromqlParamsTests extends ESTestCase { + + private static final EsqlParser parser = new EsqlParser(); + + public void testValidRangeQuery() { + PromqlCommand promql = parse("TS test | PROMQL start `2025-10-31T00:00:00Z` end `2025-10-31T01:00:00Z` step 1m (avg(foo))"); + assertThat(promql.start(), equalTo(Instant.parse("2025-10-31T00:00:00Z"))); + assertThat(promql.end(), equalTo(Instant.parse("2025-10-31T01:00:00Z"))); + assertThat(promql.step(), equalTo(Duration.ofMinutes(1))); + assertThat(promql.isRangeQuery(), equalTo(true)); + assertThat(promql.isInstantQuery(), equalTo(false)); + } + + public void testValidRangeQueryOnlyStep() { + PromqlCommand promql = parse("TS test | PROMQL step 1 (avg(foo))"); + assertThat(promql.start(), nullValue()); + assertThat(promql.end(), nullValue()); + assertThat(promql.step(), equalTo(Duration.ofSeconds(1))); + assertThat(promql.isRangeQuery(), equalTo(true)); + assertThat(promql.isInstantQuery(), equalTo(false)); + } + + public void testValidInstantQuery() { + PromqlCommand promql = parse("TS test | PROMQL time `2025-10-31T00:00:00Z` (avg(foo))"); + assertThat(promql.start(), nullValue()); + assertThat(promql.end(), nullValue()); + assertThat(promql.time(), equalTo(Instant.parse("2025-10-31T00:00:00Z"))); + assertThat(promql.step(), nullValue()); + assertThat(promql.isInstantQuery(), equalTo(true)); + assertThat(promql.isRangeQuery(), equalTo(false)); + } + + // TODO nicer error messages for missing params + public void testMissingParams() { + assertThrows(ParsingException.class, () -> parse("TS test | PROMQL (avg(foo))")); + } + + public void testZeroStep() { + ParsingException e = assertThrows(ParsingException.class, () -> parse("TS test | PROMQL step 0 (avg(foo))")); + assertThat( + e.getMessage(), + containsString( + "1:11: invalid parameter \"step\": zero or negative query resolution step widths are not accepted. " + + "Try a positive integer" + ) + ); + } + + public void testNegativeStep() { + ParsingException e = assertThrows(ParsingException.class, () -> parse("TS test | PROMQL step `-1` (avg(foo))")); + assertThat( + e.getMessage(), + containsString("invalid parameter \"step\": zero or negative query resolution step widths are not accepted") + ); + } + + public void testEndBeforeStart() { + ParsingException e = assertThrows( + ParsingException.class, + () -> parse("TS test | PROMQL start `2025-10-31T01:00:00Z` end `2025-10-31T00:00:00Z` step 1m (avg(foo))") + ); + assertThat(e.getMessage(), containsString("1:11: invalid parameter \"end\": end timestamp must not be before start time")); + } + + public void testInstantAndRangeParams() { + ParsingException e = assertThrows( + ParsingException.class, + () -> parse( + "TS test | PROMQL start `2025-10-31T00:00:00Z` end `2025-10-31T01:00:00Z` step 1m time `2025-10-31T00:00:00Z` (avg(foo))" + ) + ); + assertThat( + e.getMessage(), + containsString("1:11: Specify either [time] for instant query or [step}], [start] or [end}] for a range query") + ); + } + + public void testDuplicateParameter() { + ParsingException e = assertThrows(ParsingException.class, () -> parse("TS test | PROMQL step 1 step 2 (avg(foo))")); + assertThat(e.getMessage(), containsString("[step] already specified")); + } + + public void testUnknownParameter() { + ParsingException e = assertThrows(ParsingException.class, () -> parse("TS test | PROMQL stp 1 (avg(foo))")); + assertThat(e.getMessage(), containsString("Unknown parameter [stp], did you mean [step]?")); + } + + public void testUnknownParameterNoSuggestion() { + ParsingException e = assertThrows(ParsingException.class, () -> parse("TS test | PROMQL foo 1 (avg(foo))")); + assertThat(e.getMessage(), containsString("Unknown parameter [foo]")); + } + + public void testInvalidDateFormat() { + ParsingException e = assertThrows( + ParsingException.class, + () -> parse("TS test | PROMQL start `not-a-date` end `2025-10-31T01:00:00Z` step 1m (avg(foo))") + ); + assertThat(e.getMessage(), containsString("1:24: Invalid date format [not-a-date]")); + } + + public void testOnlyStartSpecified() { + ParsingException e = assertThrows( + ParsingException.class, + () -> parse("TS test | PROMQL start `2025-10-31T00:00:00Z` step 1m (avg(foo))") + ); + assertThat( + e.getMessage(), + containsString("Parameters [start] and [end] must either both be specified or both be omitted for a range query") + ); + } + + public void testOnlyEndSpecified() { + ParsingException e = assertThrows( + ParsingException.class, + () -> parse("TS test | PROMQL end `2025-10-31T01:00:00Z` step 1m (avg(foo))") + ); + assertThat( + e.getMessage(), + containsString("Parameters [start] and [end] must either both be specified or both be omitted for a range query") + ); + } + + public void testRangeQueryMissingStep() { + ParsingException e = assertThrows( + ParsingException.class, + () -> parse("TS test | PROMQL start `2025-10-31T00:00:00Z` end `2025-10-31T01:00:00Z` (avg(foo))") + ); + assertThat(e.getMessage(), containsString("Parameter [step] or [time] is required")); + } + + private static PromqlCommand parse(String query) { + return as(parser.createStatement(query), PromqlCommand.class); + } + + @Override + protected List filteredWarnings() { + return withDefaultLimitWarning(super.filteredWarnings()); + } + +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/ParsingUtilTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/PromqlParserUtilsTests.java similarity index 93% rename from x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/ParsingUtilTests.java rename to x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/PromqlParserUtilsTests.java index ac6ce60bf453b..c27c4a76f9bb2 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/ParsingUtilTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/promql/PromqlParserUtilsTests.java @@ -24,7 +24,7 @@ import static java.util.Arrays.asList; import static org.hamcrest.Matchers.containsString; -public class ParsingUtilTests extends ESTestCase { +public class PromqlParserUtilsTests extends ESTestCase { private Duration parseTimeValue(String value) { return PromqlParserUtils.parseDuration(new Source(0, 0, value), value); @@ -53,24 +53,14 @@ public void testTimeValueCombined() throws Exception { assertEquals(ofDays(365).plus(ofSeconds(6)), parseTimeValue("1y6s")); assertEquals(ofDays(365).plus(ofMillis(7)), parseTimeValue("1y7ms")); - assertEquals( - ofDays(365).plus(ofDays(14)).plus(ofDays(3)), - parseTimeValue("1y2w3d") - ); + assertEquals(ofDays(365).plus(ofDays(14)).plus(ofDays(3)), parseTimeValue("1y2w3d")); - assertEquals( - ofDays(365).plus(ofDays(3)).plus(ofHours(4)), - parseTimeValue("1y3d4h") - ); + assertEquals(ofDays(365).plus(ofDays(3)).plus(ofHours(4)), parseTimeValue("1y3d4h")); - assertEquals( - ofDays(365).plus(ofMinutes(5)).plus(ofSeconds(6)), - parseTimeValue("1y5m6s") - ); + assertEquals(ofDays(365).plus(ofMinutes(5)).plus(ofSeconds(6)), parseTimeValue("1y5m6s")); assertEquals( - ofDays(365).plus(ofDays(7)).plus(ofDays(1)).plus(ofHours(1)) - .plus(ofMinutes(1)).plus(ofSeconds(1)).plus(ofMillis(1)), + ofDays(365).plus(ofDays(7)).plus(ofDays(1)).plus(ofHours(1)).plus(ofMinutes(1)).plus(ofSeconds(1)).plus(ofMillis(1)), parseTimeValue("1y1w1d1h1m1s1ms") );