|
42 | 42 | import org.elasticsearch.xpack.esql.core.expression.UnresolvedStar; |
43 | 43 | import org.elasticsearch.xpack.esql.core.tree.Source; |
44 | 44 | import org.elasticsearch.xpack.esql.core.type.DataType; |
| 45 | +import org.elasticsearch.xpack.esql.core.util.CollectionUtils; |
45 | 46 | import org.elasticsearch.xpack.esql.core.util.Holder; |
| 47 | +import org.elasticsearch.xpack.esql.core.util.StringUtils; |
46 | 48 | import org.elasticsearch.xpack.esql.expression.Order; |
47 | 49 | import org.elasticsearch.xpack.esql.expression.UnresolvedNamePattern; |
48 | 50 | import org.elasticsearch.xpack.esql.expression.function.UnresolvedFunction; |
|
51 | 53 | import org.elasticsearch.xpack.esql.expression.predicate.logical.Not; |
52 | 54 | import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals; |
53 | 55 | import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.EsqlBinaryComparison; |
54 | | -import org.elasticsearch.xpack.esql.parser.promql.PromqlParams; |
55 | 56 | import org.elasticsearch.xpack.esql.parser.promql.PromqlParserUtils; |
56 | 57 | import org.elasticsearch.xpack.esql.plan.EsqlStatement; |
57 | 58 | import org.elasticsearch.xpack.esql.plan.IndexPattern; |
|
87 | 88 | import org.elasticsearch.xpack.esql.plan.logical.inference.Rerank; |
88 | 89 | import org.elasticsearch.xpack.esql.plan.logical.join.LookupJoin; |
89 | 90 | import org.elasticsearch.xpack.esql.plan.logical.promql.PromqlCommand; |
| 91 | +import org.elasticsearch.xpack.esql.plan.logical.promql.PromqlParams; |
90 | 92 | import org.elasticsearch.xpack.esql.plan.logical.show.ShowInfo; |
91 | 93 | import org.joni.exception.SyntaxException; |
92 | 94 |
|
| 95 | +import java.time.Duration; |
| 96 | +import java.time.Instant; |
93 | 97 | import java.util.ArrayList; |
94 | 98 | import java.util.Arrays; |
95 | 99 | import java.util.Collections; |
96 | 100 | import java.util.HashMap; |
| 101 | +import java.util.HashSet; |
97 | 102 | import java.util.LinkedHashMap; |
98 | 103 | import java.util.LinkedHashSet; |
99 | 104 | import java.util.List; |
|
116 | 121 | */ |
117 | 122 | public class LogicalPlanBuilder extends ExpressionBuilder { |
118 | 123 |
|
| 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 | + |
119 | 127 | /** |
120 | 128 | * Maximum number of commands allowed per query |
121 | 129 | */ |
@@ -1218,7 +1226,7 @@ public PlanFactory visitSampleCommand(EsqlBaseParser.SampleCommandContext ctx) { |
1218 | 1226 | @Override |
1219 | 1227 | public PlanFactory visitPromqlCommand(EsqlBaseParser.PromqlCommandContext ctx) { |
1220 | 1228 | Source source = source(ctx); |
1221 | | - PromqlParams params = PromqlParams.parse(ctx, source); |
| 1229 | + PromqlParams params = parse(ctx, source); |
1222 | 1230 |
|
1223 | 1231 | // TODO: Perform type and value validation |
1224 | 1232 | var queryCtx = ctx.promqlQueryPart(); |
@@ -1247,7 +1255,94 @@ public PlanFactory visitPromqlCommand(EsqlBaseParser.PromqlCommandContext ctx) { |
1247 | 1255 | } |
1248 | 1256 |
|
1249 | 1257 | 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; |
1250 | 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); |
1251 | 1339 | } |
1252 | 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 | + } |
1253 | 1348 | } |
0 commit comments