Skip to content

Commit e8f8c46

Browse files
groupings referenced in aggregations explicitly
1 parent ca1ebee commit e8f8c46

File tree

2 files changed

+135
-6
lines changed

2 files changed

+135
-6
lines changed

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.elasticsearch.xpack.esql.analysis.AnalyzerRules.ParameterizedAnalyzerRule;
2121
import org.elasticsearch.xpack.esql.common.Failure;
2222
import org.elasticsearch.xpack.esql.core.capabilities.Resolvables;
23+
import org.elasticsearch.xpack.esql.core.capabilities.Unresolvable;
2324
import org.elasticsearch.xpack.esql.core.expression.Alias;
2425
import org.elasticsearch.xpack.esql.core.expression.Attribute;
2526
import org.elasticsearch.xpack.esql.core.expression.EmptyAttribute;
@@ -588,16 +589,28 @@ private Aggregate resolveAggregate(Aggregate aggregate, List<Attribute> children
588589
// If the groupings are not resolved, skip the resolution of the references to groupings in the aggregates, resolve the
589590
// aggregations besides groupings, so that the fields and attributes referenced by the aggregations can be resolved, and
590591
// verifier doesn't report field/reference/column not found errors for them.
591-
int size = Resolvables.resolved(groupings) ? aggregates.size() : aggregates.size() - groupings.size();
592+
boolean groupingResolved = Resolvables.resolved(groupings);
593+
int size = groupingResolved ? aggregates.size() : aggregates.size() - groupings.size();
592594
for (int i = 0; i < aggregates.size(); i++) {
593595
NamedExpression ag = aggregates.get(i);
594596
if (i < size) {
595597
var agg = (NamedExpression) ag.transformUp(UnresolvedAttribute.class, ua -> {
596598
Expression ne = ua;
597599
Attribute maybeResolved = maybeResolveAttribute(ua, resolvedList);
598-
if (maybeResolved != null) {
599-
changed.set(true);
600-
ne = maybeResolved;
600+
if (groupingResolved) {
601+
if (maybeResolved != null) {
602+
changed.set(true);
603+
ne = maybeResolved;
604+
}
605+
} else {
606+
// an item in aggregations can reference to groupings explicitly, is groupings are not resolved yet and
607+
// maybeResolved is not resolved, return the original UnresolvedAttribute, so that it has a another chance
608+
// to get resolved. For example, {@code }
609+
// STATS c = count(emp_no), x = d::int + 1 BY d = (date == "2025-01-01")
610+
if (maybeResolved instanceof Unresolvable == false) {
611+
changed.set(true);
612+
ne = maybeResolved;
613+
}
601614
}
602615
return ne;
603616
});

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

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@
4747
import org.elasticsearch.xpack.esql.expression.function.fulltext.MatchOperator;
4848
import org.elasticsearch.xpack.esql.expression.function.fulltext.MultiMatch;
4949
import org.elasticsearch.xpack.esql.expression.function.fulltext.QueryString;
50+
import org.elasticsearch.xpack.esql.expression.function.grouping.Bucket;
51+
import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToInteger;
5052
import org.elasticsearch.xpack.esql.expression.function.scalar.string.Concat;
5153
import org.elasticsearch.xpack.esql.expression.function.scalar.string.Substring;
54+
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Add;
5255
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals;
5356
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.GreaterThan;
5457
import org.elasticsearch.xpack.esql.index.EsIndex;
@@ -80,6 +83,7 @@
8083
import org.elasticsearch.xpack.esql.session.IndexResolver;
8184

8285
import java.io.IOException;
86+
import java.time.Period;
8387
import java.util.ArrayList;
8488
import java.util.List;
8589
import java.util.Map;
@@ -111,6 +115,7 @@
111115
import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.tsdbIndexResolution;
112116
import static org.elasticsearch.xpack.esql.core.tree.Source.EMPTY;
113117
import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME;
118+
import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_PERIOD;
114119
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToString;
115120
import static org.hamcrest.Matchers.contains;
116121
import static org.hamcrest.Matchers.containsString;
@@ -3778,7 +3783,7 @@ public void testResolveCompletionOutputField() {
37783783
assertThat(getAttributeByName(esRelation.output(), "description"), not(equalTo(completion.targetField())));
37793784
}
37803785

3781-
public void testResolveAggregateWithGroupings() {
3786+
public void testResolveGroupingsBeforeResolvingImplicitReferencesToGroupings() {
37823787
var plan = analyze("""
37833788
FROM test
37843789
| EVAL date = "2025-01-01"::datetime
@@ -3798,7 +3803,7 @@ public void testResolveAggregateWithGroupings() {
37983803
assertEquals("d", ra.name());
37993804
List<Expression> groupings = agg.groupings();
38003805
assertEquals(1, groupings.size());
3801-
a = as(groupings.get(0), Alias.class); // reference in grouping is resolved
3806+
a = as(groupings.get(0), Alias.class); // reference in groupings is resolved
38023807
assertEquals("d", ra.name());
38033808
Equals equals = as(a.child(), Equals.class);
38043809
ra = as(equals.left(), ReferenceAttribute.class);
@@ -3808,6 +3813,117 @@ public void testResolveAggregateWithGroupings() {
38083813
assertEquals(DATETIME, literal.dataType());
38093814
}
38103815

3816+
public void testResolveGroupingsBeforeResolvingExplicitReferencesToGroupings() {
3817+
var plan = analyze("""
3818+
FROM test
3819+
| EVAL date = "2025-01-01"::datetime
3820+
| STATS c = count(emp_no), x = d::int + 1 BY d = (date == "2025-01-01")
3821+
""", "mapping-default.json");
3822+
3823+
var limit = as(plan, Limit.class);
3824+
var agg = as(limit.child(), Aggregate.class);
3825+
var aggregates = agg.aggregates();
3826+
assertThat(aggregates, hasSize(3));
3827+
Alias a = as(aggregates.get(0), Alias.class);
3828+
assertEquals("c", a.name());
3829+
Count c = as(a.child(), Count.class);
3830+
FieldAttribute fa = as(c.field(), FieldAttribute.class);
3831+
assertEquals("emp_no", fa.name());
3832+
a = as(aggregates.get(1), Alias.class); // explicit reference to groupings is resolved
3833+
assertEquals("x", a.name());
3834+
Add add = as(a.child(), Add.class);
3835+
ToInteger toInteger = as(add.left(), ToInteger.class);
3836+
ReferenceAttribute ra = as(toInteger.field(), ReferenceAttribute.class);
3837+
assertEquals("d", ra.name());
3838+
ra = as(aggregates.get(2), ReferenceAttribute.class); // reference in aggregates is resolved
3839+
assertEquals("d", ra.name());
3840+
List<Expression> groupings = agg.groupings();
3841+
assertEquals(1, groupings.size());
3842+
a = as(groupings.get(0), Alias.class); // reference in groupings is resolved
3843+
assertEquals("d", ra.name());
3844+
Equals equals = as(a.child(), Equals.class);
3845+
ra = as(equals.left(), ReferenceAttribute.class);
3846+
assertEquals("date", ra.name());
3847+
Literal literal = as(equals.right(), Literal.class);
3848+
assertEquals("2025-01-01T00:00:00.000Z", dateTimeToString(Long.parseLong(literal.value().toString())));
3849+
assertEquals(DATETIME, literal.dataType());
3850+
}
3851+
3852+
public void testBucketWithIntervalInStringInBothAggregationAndGrouping() {
3853+
var plan = analyze("""
3854+
FROM test
3855+
| STATS c = count(emp_no), b = BUCKET(hire_date, "1 year") + 1 year BY yr = BUCKET(hire_date, "1 year")
3856+
""", "mapping-default.json");
3857+
3858+
var limit = as(plan, Limit.class);
3859+
var agg = as(limit.child(), Aggregate.class);
3860+
var aggregates = agg.aggregates();
3861+
assertThat(aggregates, hasSize(3));
3862+
Alias a = as(aggregates.get(0), Alias.class);
3863+
assertEquals("c", a.name());
3864+
Count c = as(a.child(), Count.class);
3865+
FieldAttribute fa = as(c.field(), FieldAttribute.class);
3866+
assertEquals("emp_no", fa.name());
3867+
a = as(aggregates.get(1), Alias.class); // explicit reference to groupings is resolved
3868+
assertEquals("b", a.name());
3869+
Add add = as(a.child(), Add.class);
3870+
Bucket bucket = as(add.left(), Bucket.class);
3871+
fa = as(bucket.field(), FieldAttribute.class);
3872+
assertEquals("hire_date", fa.name());
3873+
Literal literal = as(bucket.buckets(), Literal.class);
3874+
Literal oneYear = new Literal(EMPTY, Period.ofYears(1), DATE_PERIOD);
3875+
assertEquals(oneYear, literal);
3876+
literal = as(add.right(), Literal.class);
3877+
assertEquals(oneYear, literal);
3878+
ReferenceAttribute ra = as(aggregates.get(2), ReferenceAttribute.class); // reference in aggregates is resolved
3879+
assertEquals("yr", ra.name());
3880+
List<Expression> groupings = agg.groupings();
3881+
assertEquals(1, groupings.size());
3882+
a = as(groupings.get(0), Alias.class); // reference in groupings is resolved
3883+
assertEquals("yr", ra.name());
3884+
bucket = as(a.child(), Bucket.class);
3885+
fa = as(bucket.field(), FieldAttribute.class);
3886+
assertEquals("hire_date", fa.name());
3887+
literal = as(bucket.buckets(), Literal.class);
3888+
assertEquals(oneYear, literal);
3889+
}
3890+
3891+
public void testBucketWithIntervalInStringInGroupingReferencedInAggregation() {
3892+
var plan = analyze("""
3893+
FROM test
3894+
| STATS c = count(emp_no), b = yr + 1 year BY yr = BUCKET(hire_date, "1 year")
3895+
""", "mapping-default.json");
3896+
3897+
var limit = as(plan, Limit.class);
3898+
var agg = as(limit.child(), Aggregate.class);
3899+
var aggregates = agg.aggregates();
3900+
assertThat(aggregates, hasSize(3));
3901+
Alias a = as(aggregates.get(0), Alias.class);
3902+
assertEquals("c", a.name());
3903+
Count c = as(a.child(), Count.class);
3904+
FieldAttribute fa = as(c.field(), FieldAttribute.class);
3905+
assertEquals("emp_no", fa.name());
3906+
a = as(aggregates.get(1), Alias.class); // explicit reference to groupings is resolved
3907+
assertEquals("b", a.name());
3908+
Add add = as(a.child(), Add.class);
3909+
ReferenceAttribute ra = as(add.left(), ReferenceAttribute.class);
3910+
assertEquals("yr", ra.name());
3911+
Literal oneYear = new Literal(EMPTY, Period.ofYears(1), DATE_PERIOD);
3912+
Literal literal = as(add.right(), Literal.class);
3913+
assertEquals(oneYear, literal);
3914+
ra = as(aggregates.get(2), ReferenceAttribute.class); // reference in aggregates is resolved
3915+
assertEquals("yr", ra.name());
3916+
List<Expression> groupings = agg.groupings();
3917+
assertEquals(1, groupings.size());
3918+
a = as(groupings.get(0), Alias.class); // reference in groupings is resolved
3919+
assertEquals("yr", ra.name());
3920+
Bucket bucket = as(a.child(), Bucket.class);
3921+
fa = as(bucket.field(), FieldAttribute.class);
3922+
assertEquals("hire_date", fa.name());
3923+
literal = as(bucket.buckets(), Literal.class);
3924+
assertEquals(oneYear, literal);
3925+
}
3926+
38113927
@Override
38123928
protected IndexAnalyzers createDefaultIndexAnalyzers() {
38133929
return super.createDefaultIndexAnalyzers();

0 commit comments

Comments
 (0)