Skip to content

Commit 1fdf620

Browse files
committed
Squashed commit of fang's subquery branch as of Monday 29th September
1 parent 87f431e commit 1fdf620

File tree

32 files changed

+4327
-225
lines changed

32 files changed

+4327
-225
lines changed

x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeForkRestTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ protected void shouldSkipTest(String testName) throws IOException {
6262
testCase.requiredCapabilities.contains(VIEWS_V1.capabilityName())
6363
);
6464

65+
assumeFalse(
66+
"Tests using subqueries are skipped since we don't support nested subqueries",
67+
testCase.requiredCapabilities.contains(SUBQUERY_IN_FROM_COMMAND.capabilityName())
68+
);
69+
6570
assumeTrue("Cluster needs to support FORK", hasCapabilities(adminClient(), List.of(FORK_V9.capabilityName())));
6671
}
6772
}

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

Lines changed: 388 additions & 0 deletions
Large diffs are not rendered by default.

x-pack/plugin/esql/src/main/antlr/EsqlBaseParser.g4

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,21 @@ timeSeriesCommand
108108
: TS indexPatternAndMetadataFields
109109
;
110110

111-
indexPatternAndMetadataFields:
112-
indexPattern (COMMA indexPattern)* metadata?
111+
indexPatternAndMetadataFields
112+
: indexPatternOrSubquery (COMMA indexPatternOrSubquery)* metadata?
113+
;
114+
115+
indexPatternOrSubquery
116+
: indexPattern
117+
| {this.isDevVersion()}? subquery
118+
;
119+
120+
subquery
121+
: LP fromCommand (PIPE subqueryProcessingCommand)* RP
122+
;
123+
124+
subqueryProcessingCommand
125+
: processingCommand
113126
;
114127

115128
indexPattern

x-pack/plugin/esql/src/main/antlr/lexer/From.g4

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ FROM_ASSIGN : ASSIGN -> type(ASSIGN);
2323
METADATA : 'metadata';
2424

2525
// we need this for EXPLAIN
26-
FROM_RP : RP -> type(RP), popMode;
26+
// change to double popMode to accommodate subquerys in FROM, when see ')' pop out of subquery(default) mode and from mode
27+
FROM_RP : RP -> type(RP), popMode, popMode;
28+
29+
// accommodate subQuery inside FROM
30+
FROM_LP : LP -> type(LP), pushMode(DEFAULT_MODE);
2731

2832
// in 8.14 ` were not allowed
2933
// this has been relaxed in 8.15 since " is used for quoting

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,11 @@ public enum Cap {
11251125
*/
11261126
VIEWS_V1(Build.current().isSnapshot()),
11271127

1128+
/**
1129+
* Support non-correlated subqueries in the FROM clause.
1130+
*/
1131+
SUBQUERY_IN_FROM_COMMAND(Build.current().isSnapshot()),
1132+
11281133
/**
11291134
* Support for the {@code leading_zeros} named parameter.
11301135
*/

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlExecutionInfo.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ public boolean includeCCSMetadata() {
151151
return includeCCSMetadata;
152152
}
153153

154+
public Predicate<String> skipOnFailurePredicate() {
155+
return skipOnFailurePredicate;
156+
}
157+
154158
/**
155159
* Call when ES|QL "planning" phase is complete and query execution (in ComputeService) is about to start.
156160
* Note this is currently only built for a single phase planning/execution model. When INLINE STATS

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Analyzer.java

Lines changed: 346 additions & 8 deletions
Large diffs are not rendered by default.

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/AnalyzerContext.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public record AnalyzerContext(
2020
IndexResolution indexResolution,
2121
Map<String, IndexResolution> lookupResolution,
2222
EnrichResolution enrichResolution,
23-
InferenceResolution inferenceResolution
23+
InferenceResolution inferenceResolution,
24+
Map<String, IndexResolution> subqueryResolution
2425
) {
2526
// Currently for tests only, since most do not test lookups
2627
// TODO: make this even simpler, remove the enrichResolution for tests that do not require it (most tests)
@@ -31,6 +32,17 @@ public AnalyzerContext(
3132
EnrichResolution enrichResolution,
3233
InferenceResolution inferenceResolution
3334
) {
34-
this(configuration, functionRegistry, indexResolution, Map.of(), enrichResolution, inferenceResolution);
35+
this(configuration, functionRegistry, indexResolution, Map.of(), enrichResolution, inferenceResolution, Map.of());
36+
}
37+
38+
public AnalyzerContext(
39+
Configuration configuration,
40+
EsqlFunctionRegistry functionRegistry,
41+
IndexResolution indexResolution,
42+
Map<String, IndexResolution> lookupResolution,
43+
EnrichResolution enrichResolution,
44+
InferenceResolution inferenceResolution
45+
) {
46+
this(configuration, functionRegistry, indexResolution, lookupResolution, enrichResolution, inferenceResolution, Map.of());
3547
}
3648
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/PreAnalyzer.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package org.elasticsearch.xpack.esql.analysis;
99

1010
import org.elasticsearch.index.IndexMode;
11+
import org.elasticsearch.xpack.esql.action.EsqlCapabilities;
1112
import org.elasticsearch.xpack.esql.core.util.Holder;
1213
import org.elasticsearch.xpack.esql.expression.function.UnresolvedFunction;
1314
import org.elasticsearch.xpack.esql.plan.IndexPattern;
@@ -16,7 +17,9 @@
1617
import org.elasticsearch.xpack.esql.plan.logical.UnresolvedRelation;
1718

1819
import java.util.ArrayList;
20+
import java.util.HashSet;
1921
import java.util.List;
22+
import java.util.Set;
2023

2124
/**
2225
* This class is part of the planner. Acts somewhat like a linker, to find the indices and enrich policies referenced by the query.
@@ -29,9 +32,10 @@ public record PreAnalysis(
2932
List<Enrich> enriches,
3033
List<IndexPattern> lookupIndices,
3134
boolean supportsAggregateMetricDouble,
32-
boolean supportsDenseVector
35+
boolean supportsDenseVector,
36+
Set<IndexPattern> subqueryIndices
3337
) {
34-
public static final PreAnalysis EMPTY = new PreAnalysis(null, null, List.of(), List.of(), false, false);
38+
public static final PreAnalysis EMPTY = new PreAnalysis(null, null, List.of(), List.of(), false, false, Set.of());
3539
}
3640

3741
public PreAnalysis preAnalyze(LogicalPlan plan) {
@@ -46,12 +50,18 @@ protected PreAnalysis doPreAnalyze(LogicalPlan plan) {
4650
Holder<IndexMode> indexMode = new Holder<>();
4751
Holder<IndexPattern> index = new Holder<>();
4852
List<IndexPattern> lookupIndices = new ArrayList<>();
53+
Set<IndexPattern> subqueryIndices = new HashSet<>();
4954
plan.forEachUp(UnresolvedRelation.class, p -> {
5055
if (p.indexMode() == IndexMode.LOOKUP) {
5156
lookupIndices.add(p.indexPattern());
5257
} else if (indexMode.get() == null || indexMode.get() == p.indexMode()) {
5358
indexMode.set(p.indexMode());
54-
index.set(p.indexPattern());
59+
// the index pattern from main query is always the first to be seen
60+
index.setIfAbsent(p.indexPattern());
61+
// collect subquery index patterns
62+
if (EsqlCapabilities.Cap.SUBQUERY_IN_FROM_COMMAND.isEnabled()) {
63+
collectSubqueryIndexPattern(p, subqueryIndices, index.get());
64+
}
5565
} else {
5666
throw new IllegalStateException("index mode is already set");
5767
}
@@ -96,7 +106,32 @@ protected PreAnalysis doPreAnalyze(LogicalPlan plan) {
96106
unresolvedEnriches,
97107
lookupIndices,
98108
indexMode.get() == IndexMode.TIME_SERIES || supportsAggregateMetricDouble.get(),
99-
supportsDenseVector.get()
109+
supportsDenseVector.get(),
110+
subqueryIndices
100111
);
101112
}
113+
114+
private void collectSubqueryIndexPattern(
115+
UnresolvedRelation relation,
116+
Set<IndexPattern> subqueryIndices,
117+
IndexPattern mainIndexPattern
118+
) {
119+
if (relation.preAnalyzed()) {
120+
return;
121+
}
122+
123+
IndexPattern pattern = relation.indexPattern();
124+
boolean isLookup = relation.indexMode() == IndexMode.LOOKUP;
125+
boolean isMainIndexPattern = pattern == mainIndexPattern;
126+
/*if the subquery's index pattern is the same as the main query, it won't be added
127+
* to the subquery indices set, if Analyzer doesn't find the subquery' indexResolution,
128+
* it falls back to the main query's indexResolution
129+
*/
130+
if (isLookup || isMainIndexPattern) {
131+
return;
132+
}
133+
subqueryIndices.add(pattern);
134+
System.out.println("collected subquery index pattern: " + pattern);
135+
System.out.println("subquery indices now: " + subqueryIndices);
136+
}
102137
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFilters.java

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

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

10+
import org.elasticsearch.core.Tuple;
1011
import org.elasticsearch.xpack.esql.core.expression.Alias;
1112
import org.elasticsearch.xpack.esql.core.expression.Attribute;
1213
import org.elasticsearch.xpack.esql.core.expression.AttributeMap;
@@ -15,18 +16,22 @@
1516
import org.elasticsearch.xpack.esql.core.expression.Expressions;
1617
import org.elasticsearch.xpack.esql.core.expression.FoldContext;
1718
import org.elasticsearch.xpack.esql.core.expression.Literal;
19+
import org.elasticsearch.xpack.esql.core.expression.NamedExpression;
1820
import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute;
1921
import org.elasticsearch.xpack.esql.core.util.CollectionUtils;
2022
import org.elasticsearch.xpack.esql.expression.predicate.Predicates;
2123
import org.elasticsearch.xpack.esql.optimizer.LogicalOptimizerContext;
2224
import org.elasticsearch.xpack.esql.plan.logical.Enrich;
2325
import org.elasticsearch.xpack.esql.plan.logical.Eval;
2426
import org.elasticsearch.xpack.esql.plan.logical.Filter;
27+
import org.elasticsearch.xpack.esql.plan.logical.Limit;
2528
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
2629
import org.elasticsearch.xpack.esql.plan.logical.OrderBy;
2730
import org.elasticsearch.xpack.esql.plan.logical.Project;
2831
import org.elasticsearch.xpack.esql.plan.logical.RegexExtract;
32+
import org.elasticsearch.xpack.esql.plan.logical.Subquery;
2933
import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan;
34+
import org.elasticsearch.xpack.esql.plan.logical.UnionAll;
3035
import org.elasticsearch.xpack.esql.plan.logical.inference.InferencePlan;
3136
import org.elasticsearch.xpack.esql.plan.logical.join.InlineJoin;
3237
import org.elasticsearch.xpack.esql.plan.logical.join.Join;
@@ -109,6 +114,12 @@ protected LogicalPlan rule(Filter filter, LogicalOptimizerContext ctx) {
109114
// See also https://github.com/elastic/elasticsearch/issues/127497
110115
// Push down past INLINE STATS if the condition is on the groupings
111116
return pushDownPastJoin(filter, join, ctx.foldCtx());
117+
} else if (child instanceof UnionAll unionAll) {
118+
// Push down filters that can be evaluated using only the output of the UnionAll
119+
plan = maybePushDownPastUnionAll(filter, unionAll);
120+
} else if (child instanceof Subquery subquery) {
121+
// subquery is a placeholder, push down the filter to the child of the subquery
122+
plan = subquery.replaceChild(new Filter(filter.source(), subquery.child(), filter.condition()));
112123
}
113124
// cannot push past a Limit, this could change the tailing result set returned
114125
return plan;
@@ -283,4 +294,160 @@ private static LogicalPlan maybePushDownPastUnary(
283294
}
284295
return plan;
285296
}
297+
298+
/* Push down filters that can be evaluated by the UnionAll child/leg to each child/leg,
299+
* so that the filters can be pushed down further to the data source if possible.
300+
* Filters that cannot be pushed down remain above the UnionAll.
301+
*
302+
* The children of a UnionAll/Fork plan has a similar pattern, as Fork adds EsqlProject,
303+
* an optional Eval and Limit on top of its actual children.
304+
* UnionAll
305+
* EsqlProject
306+
* Eval (optional)
307+
* Limit
308+
* EsRelation
309+
* EsqlProject
310+
* Eval (optional)
311+
* Limit
312+
* Subquery
313+
*
314+
* Push down the filter below limit when possible
315+
*/
316+
private static LogicalPlan maybePushDownPastUnionAll(Filter filter, UnionAll unionAll) {
317+
List<Expression> pushable = new ArrayList<>();
318+
List<Expression> nonPushable = new ArrayList<>();
319+
for (Expression exp : Predicates.splitAnd(filter.condition())) {
320+
if (exp.references().subsetOf(unionAll.outputSet())) {
321+
pushable.add(exp);
322+
} else {
323+
nonPushable.add(exp);
324+
}
325+
}
326+
if (pushable.isEmpty()) {
327+
return filter; // nothing to push down
328+
}
329+
330+
// Push the filter down to each child of the UnionAll, the child of a UnionAll is always a project
331+
// followed by an optional eval and then limit added by fork and then the real child
332+
List<LogicalPlan> newChildren = new ArrayList<>();
333+
boolean changed = false;
334+
for (LogicalPlan child : unionAll.children()) {
335+
if (child instanceof Project project) {
336+
LogicalPlan newChild = maybePushDownFilterPastEvalAndLimitForUnionAllChild(pushable, project);
337+
if (newChild != child) {
338+
changed = true;
339+
}
340+
newChildren.add(newChild);
341+
} else { // unexpected pattern, just add the child as is
342+
newChildren.add(child);
343+
}
344+
}
345+
346+
if (changed == false) { // nothing changed, return the original plan
347+
return filter;
348+
}
349+
350+
LogicalPlan newUnionAll = unionAll.replaceChildren(newChildren);
351+
if (nonPushable.isEmpty()) {
352+
return newUnionAll;
353+
} else {
354+
return filter.with(newUnionAll, Predicates.combineAnd(nonPushable));
355+
}
356+
}
357+
358+
private static LogicalPlan maybePushDownFilterPastEvalAndLimitForUnionAllChild(List<Expression> pushable, Project project) {
359+
LogicalPlan child = project.child();
360+
if (child instanceof Eval eval) {
361+
return pushDownFilterPastEvalForUnionAllChild(pushable, project, eval);
362+
} else if (child instanceof Limit limit) {
363+
LogicalPlan newLimit = pushDownFilterPastLimitForUnionAllChild(pushable, limit);
364+
return project.replaceChild(newLimit);
365+
}
366+
return project;
367+
}
368+
369+
private static LogicalPlan pushDownFilterPastEvalForUnionAllChild(List<Expression> pushable, Project project, Eval eval) {
370+
AttributeMap<Expression> evalAliases = buildEvaAliases(eval);
371+
372+
Tuple<List<Expression>, List<Expression>> pushablesAndNonPushables = splitPushableAndNonPushablePredicates(
373+
pushable,
374+
project.projections(),
375+
exp -> exp.references().stream().anyMatch(evalAliases::containsKey)
376+
);
377+
378+
LogicalPlan evalChild = eval.child();
379+
380+
if (evalChild instanceof Limit limit) {
381+
LogicalPlan newLimit = pushDownFilterPastLimitForUnionAllChild(pushablesAndNonPushables.v1(), limit);
382+
LogicalPlan newEval = eval.replaceChild(newLimit);
383+
if (pushablesAndNonPushables.v2().isEmpty()) {
384+
return project.replaceChild(newEval);
385+
} else {
386+
Filter newFilter = new Filter(project.source(), newEval, Predicates.combineAnd(pushablesAndNonPushables.v2()));
387+
return project.replaceChild(newFilter);
388+
}
389+
}
390+
return project;
391+
}
392+
393+
private static LogicalPlan pushDownFilterPastLimitForUnionAllChild(List<Expression> pushable, Limit limit) {
394+
// check whether the pushable expression's attribute needs to be replaced
395+
Tuple<List<Expression>, List<Expression>> pushablesAndNonPushables = splitPushableAndNonPushablePredicates(
396+
pushable,
397+
limit.output(),
398+
exp -> exp.references().subsetOf(limit.outputSet()) == false
399+
);
400+
401+
if (pushablesAndNonPushables.v1().isEmpty() || pushablesAndNonPushables.v2().isEmpty() == false) {
402+
return limit;
403+
}
404+
Expression combined = Predicates.combineAnd(pushablesAndNonPushables.v1());
405+
Filter pushed = new Filter(limit.source(), limit.child(), combined);
406+
return limit.replaceChild(pushed);
407+
}
408+
409+
private static AttributeMap<Expression> buildEvaAliases(Eval eval) {
410+
AttributeMap.Builder<Expression> builder = AttributeMap.builder();
411+
for (Alias alias : eval.fields()) {
412+
builder.put(alias.toAttribute(), alias.child());
413+
}
414+
return builder.build();
415+
}
416+
417+
private static Tuple<List<Expression>, List<Expression>> splitPushableAndNonPushablePredicates(
418+
List<Expression> predicates,
419+
List<? extends NamedExpression> attributes,
420+
Predicate<Expression> nonPushableCheck
421+
) {
422+
List<Expression> pushable = new ArrayList<>();
423+
List<Expression> nonPushable = new ArrayList<>();
424+
for (Expression exp : predicates) {
425+
Expression replaced = replaceAttributesByName(exp, attributes);
426+
if (replaced == null || nonPushableCheck.test(replaced)) {
427+
nonPushable.add(replaced);
428+
} else {
429+
pushable.add(replaced);
430+
}
431+
}
432+
return Tuple.tuple(pushable, nonPushable);
433+
}
434+
435+
private static Expression replaceAttributesByName(Expression expr, List<? extends NamedExpression> namedExpressions) {
436+
// Collect all referenced attributes
437+
for (Attribute attr : expr.references()) {
438+
boolean found = namedExpressions.stream().anyMatch(ne -> ne.name().equals(attr.name()));
439+
if (found == false) {
440+
return null;
441+
}
442+
}
443+
// Replace attributes by name
444+
return expr.transformUp(Attribute.class, attr -> {
445+
for (NamedExpression ne : namedExpressions) {
446+
if (ne.name().equals(attr.name())) {
447+
return ne.toAttribute();
448+
}
449+
}
450+
return attr;
451+
});
452+
}
286453
}

0 commit comments

Comments
 (0)