Skip to content

Commit 6af4039

Browse files
authored
[ES|QL] Add error message when using inline stats on TS before stats (#136348)
This commit adds a clearer error message when attempting to use inline stats in a TS query, which is unsupported at the moment. Resolves #136092
1 parent da10e19 commit 6af4039

File tree

5 files changed

+130
-1
lines changed

5 files changed

+130
-1
lines changed

docs/changelog/136348.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 136348
2+
summary: Add error message when using inline stats on TS
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 136092

x-pack/plugin/esql/qa/testFixtures/src/main/resources/k8s-timeseries.csv-spec

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,3 +547,25 @@ cnt:long | cluster:keyword | pod:keyword
547547
1 | prod | two
548548
1 | prod | three
549549
;
550+
551+
inlineStatsAfterStatsCommand
552+
required_capability: ts_command_v0
553+
required_capability: tbucket
554+
TS k8s
555+
| STATS max_cost=max(last_over_time(network.eth0.tx)) BY pod, cluster, time_bucket = TBUCKET(1h)
556+
| INLINE STATS max_cluster_cost=max(max_cost) by cluster
557+
| SORT cluster, pod, time_bucket
558+
| LIMIT 10
559+
;
560+
561+
max_cost:integer | pod:keyword | time_bucket:datetime | max_cluster_cost:integer | cluster:keyword
562+
1149 | one | 2024-05-10T00:00:00.000Z | 1149 | prod
563+
928 | three | 2024-05-10T00:00:00.000Z | 1149 | prod
564+
967 | two | 2024-05-10T00:00:00.000Z | 1149 | prod
565+
1380 | one | 2024-05-10T00:00:00.000Z | 1716 | qa
566+
1241 | three | 2024-05-10T00:00:00.000Z | 1716 | qa
567+
1716 | two | 2024-05-10T00:00:00.000Z | 1716 | qa
568+
581 | one | 2024-05-10T00:00:00.000Z | 1209 | staging
569+
1209 | three | 2024-05-10T00:00:00.000Z | 1209 | staging
570+
973 | two | 2024-05-10T00:00:00.000Z | 1209 | staging
571+
;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1111
import org.elasticsearch.common.io.stream.StreamInput;
1212
import org.elasticsearch.common.io.stream.StreamOutput;
13+
import org.elasticsearch.index.IndexMode;
1314
import org.elasticsearch.xpack.esql.capabilities.PostAnalysisVerificationAware;
1415
import org.elasticsearch.xpack.esql.capabilities.TelemetryAware;
1516
import org.elasticsearch.xpack.esql.common.Failures;
@@ -248,6 +249,15 @@ public void postAnalysisVerification(Failures failures) {
248249
}
249250

250251
protected void checkTimeSeriesAggregates(Failures failures) {
252+
Holder<Boolean> isTimeSeries = new Holder<>(false);
253+
child().forEachDown(p -> {
254+
if (p instanceof EsRelation er && er.indexMode() == IndexMode.TIME_SERIES) {
255+
isTimeSeries.set(true);
256+
}
257+
});
258+
if (isTimeSeries.get()) {
259+
return;
260+
}
251261
forEachExpression(
252262
TimeSeriesAggregateFunction.class,
253263
r -> failures.add(fail(r, "time_series aggregate[{}] can only be used with the TS command", r.sourceText()))

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1212
import org.elasticsearch.common.io.stream.StreamInput;
1313
import org.elasticsearch.common.io.stream.StreamOutput;
14+
import org.elasticsearch.index.IndexMode;
15+
import org.elasticsearch.xpack.esql.capabilities.PostAnalysisPlanVerificationAware;
1416
import org.elasticsearch.xpack.esql.capabilities.TelemetryAware;
17+
import org.elasticsearch.xpack.esql.common.Failures;
1518
import org.elasticsearch.xpack.esql.core.expression.Attribute;
1619
import org.elasticsearch.xpack.esql.core.expression.Expression;
1720
import org.elasticsearch.xpack.esql.core.expression.Expressions;
1821
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
1922
import org.elasticsearch.xpack.esql.core.tree.Source;
23+
import org.elasticsearch.xpack.esql.core.util.Holder;
2024
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
2125
import org.elasticsearch.xpack.esql.plan.logical.join.InlineJoin;
2226
import org.elasticsearch.xpack.esql.plan.logical.join.Join;
@@ -27,8 +31,10 @@
2731
import java.util.ArrayList;
2832
import java.util.List;
2933
import java.util.Objects;
34+
import java.util.function.BiConsumer;
3035

3136
import static java.util.Collections.emptyList;
37+
import static org.elasticsearch.xpack.esql.common.Failure.fail;
3238
import static org.elasticsearch.xpack.esql.expression.NamedExpressions.mergeOutputAttributes;
3339

3440
/**
@@ -38,7 +44,13 @@
3844
* underlying aggregate.
3945
* </p>
4046
*/
41-
public class InlineStats extends UnaryPlan implements NamedWriteable, SurrogateLogicalPlan, TelemetryAware, SortAgnostic {
47+
public class InlineStats extends UnaryPlan
48+
implements
49+
NamedWriteable,
50+
SurrogateLogicalPlan,
51+
TelemetryAware,
52+
SortAgnostic,
53+
PostAnalysisPlanVerificationAware {
4254
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
4355
LogicalPlan.class,
4456
"InlineStats",
@@ -152,4 +164,37 @@ public boolean equals(Object obj) {
152164
InlineStats other = (InlineStats) obj;
153165
return Objects.equals(aggregate, other.aggregate);
154166
}
167+
168+
@Override
169+
public BiConsumer<LogicalPlan, Failures> postAnalysisPlanVerification() {
170+
return (p, failures) -> {
171+
// Allow inline stats to be used with TS command if it follows a STATS command
172+
// Examples:
173+
// valid: TS metrics | STATS ...
174+
// valid: TS metrics | STATS ... | INLINE STATS ...
175+
// invalid: TS metrics | INLINE STATS ...
176+
// invalid: TS metrics | INLINE STATS ... | STATS ...
177+
if (p instanceof InlineStats inlineStats) {
178+
Holder<Boolean> foundInlineStats = new Holder<>(false);
179+
Holder<Boolean> foundPreviousStats = new Holder<>(false);
180+
Holder<Boolean> isTimeSeries = new Holder<>(false);
181+
inlineStats.child().forEachUp(lp -> {
182+
if (lp instanceof Aggregate) {
183+
if (foundInlineStats.get() == false) {
184+
foundInlineStats.set(true);
185+
} else {
186+
foundPreviousStats.set(true);
187+
}
188+
} else if (lp instanceof EsRelation er && er.indexMode() == IndexMode.TIME_SERIES) {
189+
isTimeSeries.set(true);
190+
}
191+
});
192+
if (isTimeSeries.get() && foundPreviousStats.get() == false) {
193+
failures.add(
194+
fail(inlineStats, "INLINE STATS [{}] can only be used after STATS when used with TS command", this.sourceText())
195+
);
196+
}
197+
}
198+
};
199+
}
155200
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2767,6 +2767,52 @@ public void testTextEmbeddingFunctionInvalidInferenceId() {
27672767
);
27682768
}
27692769

2770+
public void testInlineStatsInTSNotAllowed() {
2771+
assertThat(error("TS test | INLINE STATS max(network.connections)", tsdb), equalTo("""
2772+
1:11: INLINE STATS [INLINE STATS max(network.connections)] \
2773+
can only be used after STATS when used with TS command"""));
2774+
2775+
assertThat(error("""
2776+
TS test |
2777+
INLINE STATS max(network.connections) |
2778+
STATS max(max_over_time(network.connections)) BY host, time_bucket = bucket(@timestamp,1minute)""", tsdb), equalTo("""
2779+
2:1: INLINE STATS [INLINE STATS max(network.connections)] \
2780+
can only be used after STATS when used with TS command"""));
2781+
2782+
assertThat(error("TS test | INLINE STATS max_bytes=max(to_long(network.bytes_in)) BY host", tsdb), equalTo("""
2783+
1:11: INLINE STATS [INLINE STATS max_bytes=max(to_long(network.bytes_in)) BY host] \
2784+
can only be used after STATS when used with TS command"""));
2785+
2786+
assertThat(error("TS test | INLINE STATS max(60 * rate(network.bytes_in)), max(network.connections)", tsdb), equalTo("""
2787+
1:11: INLINE STATS [INLINE STATS max(60 * rate(network.bytes_in)), max(network.connections)] \
2788+
can only be used after STATS when used with TS command"""));
2789+
2790+
assertThat(error("TS test METADATA _tsid | INLINE STATS cnt = count_distinct(_tsid) BY metricset, host", tsdb), equalTo("""
2791+
1:26: INLINE STATS [INLINE STATS cnt = count_distinct(_tsid) BY metricset, host] \
2792+
can only be used after STATS when used with TS command"""));
2793+
2794+
assertThat(
2795+
error("""
2796+
TS test |
2797+
INLINE STATS max_cost=max(last_over_time(network.connections)) BY host, time_bucket = bucket(@timestamp,1minute)""", tsdb),
2798+
equalTo("""
2799+
2:1: INLINE STATS [INLINE STATS max_cost=max(last_over_time(network.connections)) \
2800+
BY host, time_bucket = bucket(@timestamp,1minute)] \
2801+
can only be used after STATS when used with TS command""")
2802+
);
2803+
2804+
assertThat(
2805+
error("TS test | INLINE STATS max_bytes=max(to_long(network.bytes_in)) BY host | SORT max_bytes DESC | keep max*, host", tsdb),
2806+
equalTo("""
2807+
1:11: INLINE STATS [INLINE STATS max_bytes=max(to_long(network.bytes_in)) BY host] \
2808+
can only be used after STATS when used with TS command""")
2809+
);
2810+
2811+
assertThat(error("TS test | INLINE STATS max(network.connections) | STATS max(network.connections) by host", tsdb), equalTo("""
2812+
1:11: INLINE STATS [INLINE STATS max(network.connections)] \
2813+
can only be used after STATS when used with TS command"""));
2814+
}
2815+
27702816
private void checkVectorFunctionsNullArgs(String functionInvocation) throws Exception {
27712817
query("from test | eval similarity = " + functionInvocation, fullTextAnalyzer);
27722818
}

0 commit comments

Comments
 (0)