Skip to content

Commit 78ddc3d

Browse files
committed
Add an appropriate failure message for SORT before INLINESTATS
1 parent e3cef72 commit 78ddc3d

File tree

5 files changed

+125
-5
lines changed

5 files changed

+125
-5
lines changed

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/capabilities/PostAnalysisPlanVerificationAware.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
* Interface implemented by expressions or plans that require validation after query plan analysis,
1919
* when the indices and references have been resolved, but before the plan is transformed further by optimizations.
2020
* The interface is similar to {@link PostAnalysisVerificationAware}, but focused on the tree structure, oftentimes covering semantic
21-
* checks.
21+
* checks. Generally, whenever one needs to check the plan structure leading to a certain node, which is the node of interest, this node's
22+
* class needs to implement this interface. Otherwise it may implement {@link PostAnalysisVerificationAware}, as more convenient.
2223
*/
2324
public interface PostAnalysisPlanVerificationAware {
2425

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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.capabilities;
9+
10+
import org.elasticsearch.xpack.esql.common.Failures;
11+
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
12+
13+
import java.util.function.BiConsumer;
14+
15+
/**
16+
* Interface implemented by expressions that require validation post logical optimization,
17+
* when the plan and references have been not just resolved but also replaced.
18+
* The interface is similar to {@link PostOptimizationVerificationAware}, but focused on individual expressions or plans, typically
19+
* covering semantic checks. Generally, whenever one needs to check the plan structure leading to a certain node, which is the node of
20+
* interest, this node's class needs to implement this interface. Otherwise it may implement {@link PostOptimizationVerificationAware},
21+
* as more convenient.
22+
*/
23+
public interface PostOptimizationPlanVerificationAware {
24+
25+
/**
26+
* Validates the implementing expression - discovered failures are reported to the given {@link Failures} class.
27+
*
28+
* <p>
29+
* Example: the SORT command, {@code OrderBy}, can only be executed currently if it can be associated with a LIMIT {@code Limit}
30+
* and together transformed into a {@code TopN} (which is executable). The replacement of the LIMIT+SORT into a TopN is done at
31+
* the end of the optimization phase. This means that any SORT still existing in the plan post optimization is an error.
32+
* However, there can be a LIMIT in the plan, but separated from the SORT by an INLINEJOIN; in this case, the LIMIT cannot be
33+
* pushed down near the SORT. To inform the user how they need to modify the query so it can be run, we implement this:
34+
* <pre>
35+
* {@code
36+
*
37+
* @Override
38+
* public BiConsumer<LogicalPlan, Failures> postOptimizationPlanVerification() {
39+
* return (p, failures) -> {
40+
* if (p instanceof InlineJoin inlineJoin) {
41+
* inlineJoin.left().forEachUp(OrderBy.class, orderBy -> {
42+
* failures.add(
43+
* fail(
44+
* inlineJoin,
45+
* "unbounded sort [{}] not supported before inlinejoin [{}], move the sort after the inlinejoin",
46+
* orderBy.sourceText(),
47+
* inlineJoin.sourceText()
48+
* )
49+
* );
50+
* });
51+
* } else if (p instanceof OrderBy) {
52+
* failures.add(fail(p, "Unbounded sort not supported yet [{}] please add a limit", p.sourceText()));
53+
* }
54+
* };
55+
* }
56+
* }
57+
* </pre>
58+
* <p>
59+
* If we didn't need to check the structure of the plan, it would have sufficed to implement the
60+
* {@link PostOptimizationVerificationAware} interface, which would simply check if there is an instance of {@code OrderBy} in the
61+
* plan.
62+
*
63+
* @return a consumer that will receive a tree to check and an accumulator of failures found during inspection.
64+
*/
65+
BiConsumer<LogicalPlan, Failures> postOptimizationPlanVerification();
66+
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalVerifier.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77

88
package org.elasticsearch.xpack.esql.optimizer;
99

10+
import org.elasticsearch.xpack.esql.capabilities.PostOptimizationPlanVerificationAware;
1011
import org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware;
1112
import org.elasticsearch.xpack.esql.common.Failures;
1213
import org.elasticsearch.xpack.esql.optimizer.rules.PlanConsistencyChecker;
1314
import org.elasticsearch.xpack.esql.plan.logical.Enrich;
1415
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
1516

17+
import java.util.ArrayList;
18+
import java.util.List;
19+
import java.util.function.BiConsumer;
20+
1621
public final class LogicalVerifier extends PostOptimizationPhasePlanVerifier<LogicalPlan> {
1722

1823
public static final LogicalVerifier INSTANCE = new LogicalVerifier();
@@ -33,19 +38,26 @@ boolean skipVerification(LogicalPlan optimizedPlan, boolean skipRemoteEnrichVeri
3338

3439
@Override
3540
void checkPlanConsistency(LogicalPlan optimizedPlan, Failures failures, Failures depFailures) {
41+
List<BiConsumer<LogicalPlan, Failures>> checkers = new ArrayList<>();
42+
3643
optimizedPlan.forEachUp(p -> {
3744
PlanConsistencyChecker.checkPlan(p, depFailures);
3845

3946
if (failures.hasFailures() == false) {
4047
if (p instanceof PostOptimizationVerificationAware pova) {
4148
pova.postOptimizationVerification(failures);
4249
}
50+
if (p instanceof PostOptimizationPlanVerificationAware popva) {
51+
checkers.add(popva.postOptimizationPlanVerification());
52+
}
4353
p.forEachExpression(ex -> {
4454
if (ex instanceof PostOptimizationVerificationAware va) {
4555
va.postOptimizationVerification(failures);
4656
}
4757
});
4858
}
4959
});
60+
61+
optimizedPlan.forEachUp(p -> checkers.forEach(checker -> checker.accept(p, failures)));
5062
}
5163
}

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import org.elasticsearch.common.io.stream.StreamInput;
1111
import org.elasticsearch.common.io.stream.StreamOutput;
1212
import org.elasticsearch.xpack.esql.capabilities.PostAnalysisVerificationAware;
13-
import org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware;
13+
import org.elasticsearch.xpack.esql.capabilities.PostOptimizationPlanVerificationAware;
1414
import org.elasticsearch.xpack.esql.capabilities.TelemetryAware;
1515
import org.elasticsearch.xpack.esql.common.Failures;
1616
import org.elasticsearch.xpack.esql.core.capabilities.Resolvables;
@@ -19,17 +19,19 @@
1919
import org.elasticsearch.xpack.esql.core.type.DataType;
2020
import org.elasticsearch.xpack.esql.expression.Order;
2121
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
22+
import org.elasticsearch.xpack.esql.plan.logical.join.InlineJoin;
2223

2324
import java.io.IOException;
2425
import java.util.List;
2526
import java.util.Objects;
27+
import java.util.function.BiConsumer;
2628

2729
import static org.elasticsearch.xpack.esql.common.Failure.fail;
2830

2931
public class OrderBy extends UnaryPlan
3032
implements
3133
PostAnalysisVerificationAware,
32-
PostOptimizationVerificationAware,
34+
PostOptimizationPlanVerificationAware,
3335
TelemetryAware,
3436
SortAgnostic,
3537
PipelineBreaker {
@@ -118,7 +120,26 @@ public void postAnalysisVerification(Failures failures) {
118120
}
119121

120122
@Override
121-
public void postOptimizationVerification(Failures failures) {
122-
failures.add(fail(this, "Unbounded sort not supported yet [{}] please add a limit", this.sourceText()));
123+
public BiConsumer<LogicalPlan, Failures> postOptimizationPlanVerification() {
124+
return (p, failures) -> {
125+
if (p instanceof InlineJoin inlineJoin) {
126+
// walking up the UnaryPlans only, not to warn twice on consecutive INLINESTATS
127+
inlineJoin.left().forEachUp(UnaryPlan.class, unary -> {
128+
if (unary instanceof OrderBy orderBy) {
129+
failures.add(
130+
fail(
131+
inlineJoin,
132+
"inlinestats [{}] cannot yet have an unbounded sort [{}] before it : either move the sort after it,"
133+
+ " or add a limit before the sort",
134+
inlineJoin.sourceText(),
135+
orderBy.sourceText()
136+
)
137+
);
138+
}
139+
});
140+
} else if (p instanceof OrderBy) {
141+
failures.add(fail(p, "Unbounded sort not supported yet [{}] please add a limit", p.sourceText()));
142+
}
143+
};
123144
}
124145
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/OptimizerVerificationTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,4 +428,24 @@ public void testRemoteEnrichAfterLookupJoinWithPipelineBreaker() {
428428
containsString("4:3: LOOKUP JOIN with remote indices can't be executed after [ENRICH _coordinator:languages_coord]@3:3")
429429
);
430430
}
431+
432+
public void testDanglingOrderByInInlineStats() {
433+
var analyzer = AnalyzerTestUtils.analyzer(loadMapping("mapping-default.json", "test"));
434+
435+
var err = error("""
436+
FROM test
437+
| SORT languages
438+
| INLINESTATS count(*) BY languages
439+
| INLINESTATS s = sum(salary) BY first_name
440+
""", analyzer);
441+
442+
assertThat(
443+
err,
444+
containsString(
445+
"2:3: Unbounded sort not supported yet [SORT languages] please add a limit\n"
446+
+ "line 3:3: inlinestats [INLINESTATS count(*) BY languages] cannot yet have an unbounded sort [SORT languages] before"
447+
+ " it : either move the sort after it, or add a limit before the sort"
448+
)
449+
);
450+
}
431451
}

0 commit comments

Comments
 (0)