Skip to content

Commit 1cd5a45

Browse files
committed
Move parameter parsing to LogicalPlanBuilder
1 parent 0f3f0f6 commit 1cd5a45

File tree

5 files changed

+137
-145
lines changed

5 files changed

+137
-145
lines changed

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

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
import org.elasticsearch.xpack.esql.core.expression.UnresolvedStar;
4343
import org.elasticsearch.xpack.esql.core.tree.Source;
4444
import org.elasticsearch.xpack.esql.core.type.DataType;
45+
import org.elasticsearch.xpack.esql.core.util.CollectionUtils;
4546
import org.elasticsearch.xpack.esql.core.util.Holder;
47+
import org.elasticsearch.xpack.esql.core.util.StringUtils;
4648
import org.elasticsearch.xpack.esql.expression.Order;
4749
import org.elasticsearch.xpack.esql.expression.UnresolvedNamePattern;
4850
import org.elasticsearch.xpack.esql.expression.function.UnresolvedFunction;
@@ -51,7 +53,6 @@
5153
import org.elasticsearch.xpack.esql.expression.predicate.logical.Not;
5254
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals;
5355
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison;
54-
import org.elasticsearch.xpack.esql.parser.promql.PromqlParams;
5556
import org.elasticsearch.xpack.esql.parser.promql.PromqlParserUtils;
5657
import org.elasticsearch.xpack.esql.plan.EsqlStatement;
5758
import org.elasticsearch.xpack.esql.plan.IndexPattern;
@@ -87,13 +88,17 @@
8788
import org.elasticsearch.xpack.esql.plan.logical.inference.Rerank;
8889
import org.elasticsearch.xpack.esql.plan.logical.join.LookupJoin;
8990
import org.elasticsearch.xpack.esql.plan.logical.promql.PromqlCommand;
91+
import org.elasticsearch.xpack.esql.plan.logical.promql.PromqlParams;
9092
import org.elasticsearch.xpack.esql.plan.logical.show.ShowInfo;
9193
import org.joni.exception.SyntaxException;
9294

95+
import java.time.Duration;
96+
import java.time.Instant;
9397
import java.util.ArrayList;
9498
import java.util.Arrays;
9599
import java.util.Collections;
96100
import java.util.HashMap;
101+
import java.util.HashSet;
97102
import java.util.LinkedHashMap;
98103
import java.util.LinkedHashSet;
99104
import java.util.List;
@@ -116,6 +121,9 @@
116121
*/
117122
public class LogicalPlanBuilder extends ExpressionBuilder {
118123

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+
119127
/**
120128
* Maximum number of commands allowed per query
121129
*/
@@ -1218,7 +1226,7 @@ public PlanFactory visitSampleCommand(EsqlBaseParser.SampleCommandContext ctx) {
12181226
@Override
12191227
public PlanFactory visitPromqlCommand(EsqlBaseParser.PromqlCommandContext ctx) {
12201228
Source source = source(ctx);
1221-
PromqlParams params = PromqlParams.parse(ctx, source);
1229+
PromqlParams params = parse(ctx, source);
12221230

12231231
// TODO: Perform type and value validation
12241232
var queryCtx = ctx.promqlQueryPart();
@@ -1247,7 +1255,94 @@ public PlanFactory visitPromqlCommand(EsqlBaseParser.PromqlCommandContext ctx) {
12471255
}
12481256

12491257
return plan -> new PromqlCommand(source, plan, promqlPlan, params);
1258+
}
1259+
1260+
private static PromqlParams parse(EsqlBaseParser.PromqlCommandContext ctx, Source source) {
1261+
Instant time = null;
1262+
Instant start = null;
1263+
Instant end = null;
1264+
Duration step = null;
12501265

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);
12511339
}
12521340

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+
}
12531348
}

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

Lines changed: 0 additions & 139 deletions
This file was deleted.

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,6 @@ public static Instant parseDate(Source source, String value) {
284284
} catch (NumberFormatException ignore) {
285285
// Not a float, try parsing as date string
286286
}
287-
if (value.startsWith("\"") && value.endsWith("\"")) {
288-
value = value.substring(1, value.length() - 1);
289-
}
290287
try {
291288
return Instant.parse(value);
292289
} catch (DateTimeParseException e) {

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/promql/PromqlCommand.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import org.elasticsearch.xpack.esql.common.Failures;
1414
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
1515
import org.elasticsearch.xpack.esql.core.tree.Source;
16-
import org.elasticsearch.xpack.esql.parser.promql.PromqlParams;
1716
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
1817
import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan;
1918
import org.elasticsearch.xpack.esql.plan.logical.promql.selector.Selector;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.esql.plan.logical.promql;
9+
10+
import java.time.Duration;
11+
import java.time.Instant;
12+
13+
/**
14+
* Container for PromQL command parameters:
15+
* <ul>
16+
* <li>time for instant queries</li>
17+
* <li>start, end, step for range queries</li>
18+
* </ul>
19+
* These can be specified in the {@linkplain org.elasticsearch.xpack.esql.plan.logical.promql.PromqlCommand PROMQL command} like so:
20+
* <pre>
21+
* # instant query
22+
* PROMQL time `2025-10-31T00:00:00Z` (avg(foo))
23+
* # range query with explicit start and end
24+
* PROMQL start `2025-10-31T00:00:00Z` end `2025-10-31T01:00:00Z` step 1m (avg(foo))
25+
* # range query with implicit time bounds, doesn't support calling {@code start()} or {@code end()} functions
26+
* PROMQL step 5m (avg(foo))
27+
* </pre>
28+
*
29+
* @see <a href="https://prometheus.io/docs/prometheus/latest/querying/api/#expression-queries">PromQL API documentation</a>
30+
*/
31+
public record PromqlParams(Instant time, Instant start, Instant end, Duration step) {
32+
33+
public boolean isInstantQuery() {
34+
return time != null;
35+
}
36+
37+
public boolean isRangeQuery() {
38+
return step != null;
39+
}
40+
}

0 commit comments

Comments
 (0)