Skip to content

Commit 8272dcd

Browse files
authored
Parse start/end/time/step parameters (#137472)
1 parent 2351e39 commit 8272dcd

File tree

15 files changed

+394
-153
lines changed

15 files changed

+394
-153
lines changed

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/promql/predicate/operator/VectorBinaryOperator.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
import org.elasticsearch.xpack.esql.core.expression.function.Function;
1515
import org.elasticsearch.xpack.esql.core.tree.Source;
1616
import org.elasticsearch.xpack.esql.core.type.DataType;
17-
import org.elasticsearch.xpack.esql.evaluator.mapper.EvaluatorMapper;
18-
import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction;
1917
import org.elasticsearch.xpack.esql.plan.logical.BinaryPlan;
2018
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
2119
import org.elasticsearch.xpack.esql.plan.logical.promql.selector.LabelMatcher;
@@ -163,9 +161,7 @@ public boolean equals(Object o) {
163161
if (o == null || getClass() != o.getClass()) return false;
164162
if (super.equals(o)) {
165163
VectorBinaryOperator that = (VectorBinaryOperator) o;
166-
return dropMetricName == that.dropMetricName
167-
&& Objects.equals(match, that.match)
168-
&& Objects.equals(binaryOp, that.binaryOp);
164+
return dropMetricName == that.dropMetricName && Objects.equals(match, that.match) && Objects.equals(binaryOp, that.binaryOp);
169165
}
170166
return false;
171167
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/promql/predicate/operator/comparison/VectorBinaryComparison.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,14 @@ public ScalarFunctionFactory asFunction() {
4747
private final ComparisonOp op;
4848
private final boolean boolMode;
4949

50-
public VectorBinaryComparison(Source source, LogicalPlan left, LogicalPlan right, VectorMatch match, boolean boolMode, ComparisonOp op) {
50+
public VectorBinaryComparison(
51+
Source source,
52+
LogicalPlan left,
53+
LogicalPlan right,
54+
VectorMatch match,
55+
boolean boolMode,
56+
ComparisonOp op
57+
) {
5158
super(source, left, right, match, boolMode == false, op);
5259
this.op = op;
5360
this.boolMode = boolMode;

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/LogicalPlanBuilder.java

Lines changed: 97 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,17 @@
8888
import org.elasticsearch.xpack.esql.plan.logical.inference.Rerank;
8989
import org.elasticsearch.xpack.esql.plan.logical.join.LookupJoin;
9090
import org.elasticsearch.xpack.esql.plan.logical.promql.PromqlCommand;
91+
import org.elasticsearch.xpack.esql.plan.logical.promql.PromqlParams;
9192
import org.elasticsearch.xpack.esql.plan.logical.show.ShowInfo;
9293
import org.joni.exception.SyntaxException;
9394

95+
import java.time.Duration;
96+
import java.time.Instant;
9497
import java.util.ArrayList;
9598
import java.util.Arrays;
9699
import java.util.Collections;
97100
import java.util.HashMap;
101+
import java.util.HashSet;
98102
import java.util.LinkedHashMap;
99103
import java.util.LinkedHashSet;
100104
import java.util.List;
@@ -117,6 +121,9 @@
117121
*/
118122
public class LogicalPlanBuilder extends ExpressionBuilder {
119123

124+
private static final String TIME = "time", START = "start", END = "end", STEP = "step";
125+
private static final Set<String> PROMQL_ALLOWED_PARAMS = Set.of(TIME, START, END, STEP);
126+
120127
/**
121128
* Maximum number of commands allowed per query
122129
*/
@@ -1219,52 +1226,7 @@ public PlanFactory visitSampleCommand(EsqlBaseParser.SampleCommandContext ctx) {
12191226
@Override
12201227
public PlanFactory visitPromqlCommand(EsqlBaseParser.PromqlCommandContext ctx) {
12211228
Source source = source(ctx);
1222-
Map<String, Expression> params = new HashMap<>();
1223-
String TIME = "time", START = "start", END = "end", STEP = "step";
1224-
Set<String> allowed = Set.of(TIME, START, END, STEP);
1225-
1226-
if (ctx.promqlParam().isEmpty()) {
1227-
throw new ParsingException(source(ctx), "Parameter [{}] or [{}] is required", STEP, TIME);
1228-
}
1229-
1230-
for (EsqlBaseParser.PromqlParamContext paramCtx : ctx.promqlParam()) {
1231-
var paramNameCtx = paramCtx.name;
1232-
String name = paramNameCtx.getText();
1233-
if (params.containsKey(name)) {
1234-
throw new ParsingException(source(paramNameCtx), "[{}] already specified", name);
1235-
}
1236-
if (allowed.contains(name) == false) {
1237-
String message = "Unknown parameter [{}]";
1238-
List<String> similar = StringUtils.findSimilar(name, allowed);
1239-
if (CollectionUtils.isEmpty(similar) == false) {
1240-
message += ", did you mean " + (similar.size() == 1 ? "[" + similar.get(0) + "]" : "any of " + similar) + "?";
1241-
}
1242-
throw new ParsingException(source(paramNameCtx), message, name);
1243-
}
1244-
String value = paramCtx.value.getText();
1245-
// TODO: validate and convert the value
1246-
1247-
}
1248-
1249-
// Validation logic for time parameters
1250-
Expression time = params.get(TIME);
1251-
Expression start = params.get(START);
1252-
Expression end = params.get(END);
1253-
Expression step = params.get(STEP);
1254-
1255-
if (time != null && (start != null || end != null || step != null)) {
1256-
throw new ParsingException(
1257-
source,
1258-
"Specify either [{}] for instant query or [{}}], [{}] or [{}}] for a range query",
1259-
TIME,
1260-
STEP,
1261-
START,
1262-
END
1263-
);
1264-
}
1265-
if ((start != null || end != null) && step == null) {
1266-
throw new ParsingException(source, "[{}}] is required alongside [{}}] or [{}}]", STEP, START, END);
1267-
}
1229+
PromqlParams params = parsePromqlParams(ctx, source);
12681230

12691231
// TODO: Perform type and value validation
12701232
var queryCtx = ctx.promqlQueryPart();
@@ -1292,9 +1254,95 @@ public PlanFactory visitPromqlCommand(EsqlBaseParser.PromqlCommandContext ctx) {
12921254
throw PromqlParserUtils.adjustParsingException(pe, promqlStartLine, promqlStartColumn);
12931255
}
12941256

1295-
return plan -> time != null
1296-
? new PromqlCommand(source, plan, promqlPlan, time)
1297-
: new PromqlCommand(source, plan, promqlPlan, start, end, step);
1257+
return plan -> new PromqlCommand(source, plan, promqlPlan, params);
12981258
}
12991259

1260+
private static PromqlParams parsePromqlParams(EsqlBaseParser.PromqlCommandContext ctx, Source source) {
1261+
Instant time = null;
1262+
Instant start = null;
1263+
Instant end = null;
1264+
Duration step = null;
1265+
1266+
Set<String> paramsSeen = new HashSet<>();
1267+
for (EsqlBaseParser.PromqlParamContext paramCtx : ctx.promqlParam()) {
1268+
String name = param(paramCtx.name);
1269+
if (paramsSeen.add(name) == false) {
1270+
throw new ParsingException(source(paramCtx.name), "[{}] already specified", name);
1271+
}
1272+
Source valueSource = source(paramCtx.value);
1273+
String valueString = param(paramCtx.value);
1274+
switch (name) {
1275+
case TIME -> time = PromqlParserUtils.parseDate(valueSource, valueString);
1276+
case START -> start = PromqlParserUtils.parseDate(valueSource, valueString);
1277+
case END -> end = PromqlParserUtils.parseDate(valueSource, valueString);
1278+
case STEP -> {
1279+
try {
1280+
step = Duration.ofSeconds(Integer.parseInt(valueString));
1281+
} catch (NumberFormatException ignore) {
1282+
step = PromqlParserUtils.parseDuration(valueSource, valueString);
1283+
}
1284+
}
1285+
default -> {
1286+
String message = "Unknown parameter [{}]";
1287+
List<String> similar = StringUtils.findSimilar(name, PROMQL_ALLOWED_PARAMS);
1288+
if (CollectionUtils.isEmpty(similar) == false) {
1289+
message += ", did you mean " + (similar.size() == 1 ? "[" + similar.get(0) + "]" : "any of " + similar) + "?";
1290+
}
1291+
throw new ParsingException(source(paramCtx.name), message, name);
1292+
}
1293+
}
1294+
}
1295+
1296+
// Validation logic for time parameters
1297+
if (time != null) {
1298+
if (start != null || end != null || step != null) {
1299+
throw new ParsingException(
1300+
source,
1301+
"Specify either [{}] for instant query or [{}}], [{}] or [{}}] for a range query",
1302+
TIME,
1303+
STEP,
1304+
START,
1305+
END
1306+
);
1307+
}
1308+
} else if (step != null) {
1309+
if (start != null || end != null) {
1310+
if (start == null || end == null) {
1311+
throw new ParsingException(
1312+
source,
1313+
"Parameters [{}] and [{}] must either both be specified or both be omitted for a range query",
1314+
START,
1315+
END
1316+
);
1317+
}
1318+
if (end.isBefore(start)) {
1319+
throw new ParsingException(
1320+
source,
1321+
"invalid parameter \"end\": end timestamp must not be before start time",
1322+
end,
1323+
start
1324+
);
1325+
}
1326+
}
1327+
if (step.isPositive() == false) {
1328+
throw new ParsingException(
1329+
source,
1330+
"invalid parameter \"step\": zero or negative query resolution step widths are not accepted. "
1331+
+ "Try a positive integer",
1332+
step
1333+
);
1334+
}
1335+
} else {
1336+
throw new ParsingException(source, "Parameter [{}] or [{}] is required", STEP, TIME);
1337+
}
1338+
return new PromqlParams(time, start, end, step);
1339+
}
1340+
1341+
private static String param(EsqlBaseParser.PromqlParamContentContext paramCtx) {
1342+
if (paramCtx.QUOTED_IDENTIFIER() != null) {
1343+
return AbstractBuilder.unquote(paramCtx.QUOTED_IDENTIFIER().getText());
1344+
} else {
1345+
return paramCtx.getText();
1346+
}
1347+
}
13001348
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlExpressionBuilder.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,7 @@ public Duration visitDuration(DurationContext ctx) {
173173
}
174174

175175
// Non-literal LogicalPlan
176-
throw new ParsingException(
177-
source(ctx),
178-
"Duration must be a constant expression"
179-
);
176+
throw new ParsingException(source(ctx), "Duration must be a constant expression");
180177
}
181178
case Expression e -> {
182179
// Fallback for Expression (shouldn't happen with new logic)

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlFoldingUtils.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,7 @@ private static Duration arithmetics(Source source, Duration left, Duration right
6464
Duration result = switch (op) {
6565
case ADD -> left.plus(right);
6666
case SUB -> left.minus(right);
67-
default -> throw new ParsingException(
68-
source,
69-
"Operation [{}] not supported between two durations",
70-
op
71-
);
67+
default -> throw new ParsingException(source, "Operation [{}] not supported between two durations", op);
7268
};
7369

7470
return result;
@@ -120,11 +116,7 @@ private static Duration arithmetics(Source source, Number scalar, Duration durat
120116
case ADD -> arithmetics(source, duration, scalar, ArithmeticOp.ADD);
121117
case SUB -> arithmetics(source, Duration.ofSeconds(scalar.longValue()), duration, ArithmeticOp.SUB);
122118
case MUL -> arithmetics(source, duration, scalar, ArithmeticOp.MUL);
123-
default -> throw new ParsingException(
124-
source,
125-
"Operation [{}] not supported with scalar on left and duration on right",
126-
op
127-
);
119+
default -> throw new ParsingException(source, "Operation [{}] not supported with scalar on left and duration on right", op);
128120
};
129121
}
130122

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlLogicalPlanBuilder.java

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import org.elasticsearch.xpack.esql.expression.promql.predicate.operator.comparison.VectorBinaryComparison.ComparisonOp;
2929
import org.elasticsearch.xpack.esql.expression.promql.predicate.operator.set.VectorBinarySet;
3030
import org.elasticsearch.xpack.esql.expression.promql.subquery.Subquery;
31-
import org.elasticsearch.xpack.esql.parser.EsqlBaseParser;
3231
import org.elasticsearch.xpack.esql.parser.ParsingException;
3332
import org.elasticsearch.xpack.esql.parser.PromqlBaseParser;
3433
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
@@ -296,7 +295,7 @@ public LogicalPlan visitArithmeticBinary(PromqlBaseParser.ArithmeticBinaryContex
296295
Object leftValue = leftLiteral.value();
297296
Object rightValue = rightLiteral.value();
298297

299-
// arithmetics
298+
// arithmetics
300299
if (binaryOperator instanceof ArithmeticOp arithmeticOp) {
301300
Object result = PromqlFoldingUtils.evaluate(source, leftValue, rightValue, arithmeticOp);
302301
DataType resultType = determineResultType(result);
@@ -356,20 +355,12 @@ public LogicalPlan visitArithmeticBinary(PromqlBaseParser.ArithmeticBinaryContex
356355

357356
return switch (binaryOperator) {
358357
case ArithmeticOp arithmeticOp -> new VectorBinaryArithmetic(source, le, re, modifier, arithmeticOp);
359-
case ComparisonOp comparisonOp -> new VectorBinaryComparison(
360-
source,
361-
le,
362-
re,
363-
modifier,
364-
bool,
365-
comparisonOp
366-
);
358+
case ComparisonOp comparisonOp -> new VectorBinaryComparison(source, le, re, modifier, bool, comparisonOp);
367359
case VectorBinarySet.SetOp setOp -> new VectorBinarySet(source, le, re, modifier, setOp);
368360
default -> throw new ParsingException(source(ctx.op), "Unknown arithmetic {}", opText);
369361
};
370362
}
371363

372-
373364
private BinaryOp binaryOp(Token opType) {
374365
return switch (opType.getType()) {
375366
case CARET -> ArithmeticOp.POW;
@@ -548,8 +539,7 @@ public Duration visitSubqueryResolution(PromqlBaseParser.SubqueryResolutionConte
548539
return duration;
549540
}
550541

551-
throw new ParsingException(source(ctx), "Expected duration result, got [{}]",
552-
result.getClass().getSimpleName());
542+
throw new ParsingException(source(ctx), "Expected duration result, got [{}]", result.getClass().getSimpleName());
553543
}
554544

555545
// Just COLON with no resolution - use default

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/promql/PromqlParserUtils.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import org.elasticsearch.xpack.esql.parser.ParsingException;
1414

1515
import java.time.Duration;
16+
import java.time.Instant;
17+
import java.time.format.DateTimeParseException;
1618
import java.util.LinkedHashMap;
1719
import java.util.Map;
1820

@@ -272,4 +274,20 @@ private static int adjustColumn(int lineNumber, int columnNumber, int startColum
272274
// the column offset only applies to the first line of the PROMQL command
273275
return lineNumber == 1 ? columnNumber + startColumn - 1 : columnNumber;
274276
}
277+
278+
/*
279+
* Parses a Prometheus date which can be either a float representing epoch seconds or an RFC3339 date string.
280+
*/
281+
public static Instant parseDate(Source source, String value) {
282+
try {
283+
return Instant.ofEpochMilli((long) (Double.parseDouble(value) * 1000));
284+
} catch (NumberFormatException ignore) {
285+
// Not a float, try parsing as date string
286+
}
287+
try {
288+
return Instant.parse(value);
289+
} catch (DateTimeParseException e) {
290+
throw new ParsingException(source, "Invalid date format [{}]", value);
291+
}
292+
}
275293
}

0 commit comments

Comments
 (0)