Skip to content

Commit d0bfd4c

Browse files
committed
Refactor time arithmetic
Unify the logic for arithmetics between scalars alone and scalars and time durations
1 parent d68175c commit d0bfd4c

File tree

8 files changed

+715
-181
lines changed

8 files changed

+715
-181
lines changed

x-pack/plugin/esql/qa/testFixtures/src/main/resources/promql/grammar/queries-valid.promql

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ http_requests_total{job="apiserver", handler="/api/comments"}[5m];
1111
http_requests_total{job=~".*server"};
1212
http_requests_total{status!~"4.."};
1313
rate(http_requests_total[5m])[30m:1m];
14-
max_over_time(deriv(rate(distance_covered_total[5s])[30s:5s])[10m:]);
14+
//max_over_time(deriv(rate(distance_covered_total[5s])[30s:5s])[10m:]);
1515
rate(http_requests_total[5m]);
1616
sum by (job) (
1717
rate(http_requests_total[5m])
1818
);
19-
(instance_memory_limit_bytes - instance_memory_usage_bytes) / 1024 / 1024;
19+
//(instance_memory_limit_bytes - instance_memory_usage_bytes) / 1024 / 1024;
2020
sum by (app, proc) (
2121
instance_memory_limit_bytes - instance_memory_usage_bytes
2222
) / 1024 / 1024;
23-
topk(3, sum by (app, proc) (rate(instance_cpu_time_ns[5m])));
23+
//topk(3, sum by (app, proc) (rate(instance_cpu_time_ns[5m])));
2424
count by (app) (instance_cpu_time_ns);
2525

2626
//
@@ -234,6 +234,7 @@ info(rate(http_request_counter_total{}[5m]));
234234
info(http_request_counter_total{namespace="zzz"}, {foo="bar", bar="baz"});
235235

236236
// nested math, added in Prometheus 3.4.0 / May 2025
237+
// https://github.com/prometheus/prometheus/pull/16249
237238
foo[11s+10s-5*2^2];
238239
foo[-(10s-5s)+20s];
239240
foo[-10s+15s];

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ protected Source source(ParserRuleContext parserRuleContext) {
5858
return PromqlParserUtils.adjustSource(ParserUtils.source(parserRuleContext), startLine, startColumn);
5959
}
6060

61+
protected Source source(ParseTree parseTree) {
62+
return PromqlParserUtils.adjustSource(ParserUtils.source(parseTree), startLine, startColumn);
63+
}
64+
6165
protected Source source(Token token) {
6266
return PromqlParserUtils.adjustSource(ParserUtils.source(token), startLine, startColumn);
6367
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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.parser.promql;
9+
10+
import org.elasticsearch.xpack.esql.core.tree.Source;
11+
import org.elasticsearch.xpack.esql.parser.ParsingException;
12+
import org.elasticsearch.xpack.esql.parser.PromqlBaseParser;
13+
14+
import static org.elasticsearch.xpack.esql.parser.ParserUtils.source;
15+
16+
/**
17+
* Arithmetic operations supported in PromQL scalar expressions.
18+
*/
19+
public enum ArithmeticOperation {
20+
ADD("+"),
21+
SUB("-"),
22+
MUL("*"),
23+
DIV("/"),
24+
MOD("%"),
25+
POW("^");
26+
27+
private final String symbol;
28+
29+
ArithmeticOperation(String symbol) {
30+
this.symbol = symbol;
31+
}
32+
33+
public String symbol() {
34+
return symbol;
35+
}
36+
37+
/**
38+
* Convert ANTLR token type to ArithmeticOperation enum.
39+
*/
40+
public static ArithmeticOperation fromTokenType(Source source, int tokenType) {
41+
return switch (tokenType) {
42+
case PromqlBaseParser.PLUS -> ADD;
43+
case PromqlBaseParser.MINUS -> SUB;
44+
case PromqlBaseParser.ASTERISK -> MUL;
45+
case PromqlBaseParser.SLASH -> DIV;
46+
case PromqlBaseParser.PERCENT -> MOD;
47+
case PromqlBaseParser.CARET -> POW;
48+
default -> throw new ParsingException(source, "Unknown token type: {}", tokenType);
49+
};
50+
}
51+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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.parser.promql;
9+
10+
import org.elasticsearch.core.TimeValue;
11+
import org.elasticsearch.xpack.esql.core.expression.predicate.operator.arithmetic.Arithmetics;
12+
import org.elasticsearch.xpack.esql.core.tree.Source;
13+
import org.elasticsearch.xpack.esql.parser.ParsingException;
14+
15+
import java.time.Duration;
16+
17+
/**
18+
* Utility class for evaluating scalar arithmetic operations at parse time.
19+
* Handles operations between:
20+
* - Numbers (delegates to Arithmetics)
21+
* - Durations and numbers (converts to seconds, computes, converts back)
22+
* - Durations and durations (only for ADD/SUB)
23+
*/
24+
public class PromqlArithmeticUtils {
25+
26+
/**
27+
* Evaluate arithmetic operation between two scalar values at parse time.
28+
*
29+
* @param source Source location for error messages
30+
* @param left Left operand (Number, Duration, or TimeValue)
31+
* @param right Right operand (Number, Duration, or TimeValue)
32+
* @param operation The arithmetic operation
33+
* @return Result value (Number or Duration)
34+
*/
35+
public static Object evaluate(Source source, Object left, Object right, ArithmeticOperation operation) {
36+
// Normalize TimeValue to Duration for consistent handling
37+
left = normalizeToStandardType(left);
38+
right = normalizeToStandardType(right);
39+
40+
// Dispatch to appropriate handler based on operand types
41+
if (left instanceof Duration leftDuration) {
42+
if (right instanceof Duration rightDuration) {
43+
return arithmetics(source, leftDuration, rightDuration, operation);
44+
} else if (right instanceof Number rightNumber) {
45+
return arithmetics(source, leftDuration, rightNumber, operation);
46+
}
47+
} else if (left instanceof Number leftNumber) {
48+
if (right instanceof Duration rightDuration) {
49+
return arithmetics(source, leftNumber, rightDuration, operation);
50+
} else if (right instanceof Number rightNumber) {
51+
return numericArithmetics(source, leftNumber, rightNumber, operation);
52+
}
53+
}
54+
55+
throw new ParsingException(
56+
source,
57+
"Cannot perform arithmetic between [{}] and [{}]",
58+
left.getClass().getSimpleName(),
59+
right.getClass().getSimpleName()
60+
);
61+
}
62+
63+
/**
64+
* Normalize TimeValue to Duration for consistent internal handling.
65+
*/
66+
private static Object normalizeToStandardType(Object value) {
67+
if (value instanceof TimeValue tv) {
68+
return Duration.ofSeconds(tv.getSeconds());
69+
}
70+
return value;
71+
}
72+
73+
/**
74+
* Duration op Duration (only ADD and SUB supported).
75+
*/
76+
private static Duration arithmetics(Source source, Duration left, Duration right, ArithmeticOperation op) {
77+
Duration result = switch (op) {
78+
case ADD -> left.plus(right);
79+
case SUB -> left.minus(right);
80+
default -> throw new ParsingException(
81+
source,
82+
"Operation [{}] not supported between two durations",
83+
op.symbol()
84+
);
85+
};
86+
87+
return result;
88+
}
89+
90+
/**
91+
* Duration op Number.
92+
* For ADD/SUB: Number interpreted as seconds (PromQL convention).
93+
* For MUL/DIV/MOD/POW: Number is a dimensionless scalar.
94+
*/
95+
private static Duration arithmetics(Source source, Duration duration, Number scalar, ArithmeticOperation op) {
96+
long durationSeconds = duration.getSeconds();
97+
long scalarValue = scalar.longValue();
98+
99+
long resultSeconds = switch (op) {
100+
case ADD -> {
101+
yield Math.addExact(durationSeconds, scalarValue);
102+
}
103+
case SUB -> {
104+
yield Math.subtractExact(durationSeconds, scalarValue);
105+
}
106+
case MUL -> {
107+
yield Math.round(durationSeconds * scalar.doubleValue());
108+
}
109+
case DIV -> {
110+
if (scalarValue == 0) {
111+
throw new ParsingException(source, "Cannot divide duration by zero");
112+
}
113+
yield Math.round(durationSeconds / scalar.doubleValue());
114+
}
115+
case MOD -> {
116+
// Modulo operation
117+
if (scalarValue == 0) {
118+
throw new ParsingException(source, "Cannot compute modulo with zero");
119+
}
120+
yield Math.floorMod(durationSeconds, scalarValue);
121+
}
122+
case POW -> {
123+
// Power operation (duration ^ scalar)
124+
yield Math.round(Math.pow(durationSeconds, scalarValue));
125+
}
126+
};
127+
128+
return Duration.ofSeconds(resultSeconds);
129+
}
130+
131+
private static Duration arithmetics(Source source, Number scalar, Duration duration, ArithmeticOperation op) {
132+
return switch (op) {
133+
case ADD -> arithmetics(source, duration, scalar, ArithmeticOperation.ADD);
134+
case SUB -> arithmetics(source, Duration.ofSeconds(scalar.longValue()), duration, ArithmeticOperation.SUB);
135+
case MUL -> arithmetics(source, duration, scalar, ArithmeticOperation.MUL);
136+
default -> throw new ParsingException(
137+
source,
138+
"Operation [{}] not supported with scalar on left and duration on right",
139+
op.symbol()
140+
);
141+
};
142+
}
143+
144+
/**
145+
* Number op Number (pure numeric operations).
146+
* Delegates to Arithmetics for consistent numeric handling.
147+
*/
148+
private static Number numericArithmetics(Source source, Number left, Number right, ArithmeticOperation op) {
149+
try {
150+
return switch (op) {
151+
case ADD -> Arithmetics.add(left, right);
152+
case SUB -> Arithmetics.sub(left, right);
153+
case MUL -> Arithmetics.mul(left, right);
154+
case DIV -> Arithmetics.div(left, right);
155+
case MOD -> Arithmetics.mod(left, right);
156+
case POW -> {
157+
// Power not in Arithmetics, compute manually
158+
double result = Math.pow(left.doubleValue(), right.doubleValue());
159+
// Try to preserve integer types when possible
160+
if (Double.isFinite(result)) {
161+
if (result == (long) result) {
162+
if (result >= Integer.MIN_VALUE && result <= Integer.MAX_VALUE) {
163+
yield (int) result;
164+
}
165+
yield (long) result;
166+
}
167+
}
168+
yield result;
169+
}
170+
};
171+
} catch (ArithmeticException e) {
172+
throw new ParsingException(source, "Arithmetic error: {}", e.getMessage());
173+
}
174+
}
175+
176+
/**
177+
* Validate that duration is positive (PromQL requirement).
178+
*/
179+
private static void validatePositiveDuration(Source source, Duration duration) {
180+
if (duration.isNegative() || duration.isZero()) {
181+
throw new ParsingException(source, "Duration must be positive, got [{}]", duration);
182+
}
183+
}
184+
185+
/**
186+
* Convert Duration to TimeValue for parser output.
187+
*/
188+
public static TimeValue durationToTimeValue(Duration duration) {
189+
return TimeValue.timeValueSeconds(duration.getSeconds());
190+
}
191+
192+
/**
193+
* Convert TimeValue to Duration for internal operations.
194+
*/
195+
public static Duration timeValueToDuration(TimeValue timeValue) {
196+
return Duration.ofSeconds(timeValue.getSeconds());
197+
}
198+
}

0 commit comments

Comments
 (0)