Skip to content

Commit 4cf0391

Browse files
committed
SQL: Generate relevant error message when grouping functions are not used in GROUP BY (#38017)
* Add checks for Grouping functions restriction to be placed inside GROUP BY * Fixed bug where GROUP BY HISTOGRAM (not using alias) wasn't recognized properly in the Verifier due to functions equality not working correctly. (cherry picked from commit 6968f09)
1 parent 0169965 commit 4cf0391

File tree

5 files changed

+89
-16
lines changed

5 files changed

+89
-16
lines changed

x-pack/plugin/sql/qa/src/main/resources/agg.csv-spec

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,29 @@ SELECT HISTOGRAM(emp_no % 100, 10) AS h, COUNT(*) as c FROM test_emp GROUP BY h
319319
0 |10
320320
;
321321

322+
histogramGroupByWithoutAlias
323+
schema::h:ts|c:l
324+
SELECT HISTOGRAM(birth_date, INTERVAL 1 YEAR) AS h, COUNT(*) as c FROM test_emp GROUP BY HISTOGRAM(birth_date, INTERVAL 1 YEAR) ORDER BY h DESC;
325+
326+
h | c
327+
--------------------+---------------
328+
1964-02-02T00:00:00Z|5
329+
1963-02-07T00:00:00Z|7
330+
1962-02-12T00:00:00Z|6
331+
1961-02-17T00:00:00Z|8
332+
1960-02-23T00:00:00Z|7
333+
1959-02-28T00:00:00Z|9
334+
1958-03-05T00:00:00Z|6
335+
1957-03-10T00:00:00Z|6
336+
1956-03-15T00:00:00Z|4
337+
1955-03-21T00:00:00Z|4
338+
1954-03-26T00:00:00Z|7
339+
1953-03-31T00:00:00Z|10
340+
1952-04-05T00:00:00Z|10
341+
1951-04-11T00:00:00Z|1
342+
null |10
343+
;
344+
322345
countAll
323346
schema::all_names:l|c:l
324347
SELECT COUNT(ALL first_name) all_names, COUNT(*) c FROM test_emp;

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/analysis/analyzer/Verifier.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.elasticsearch.xpack.sql.expression.function.aggregate.Max;
2424
import org.elasticsearch.xpack.sql.expression.function.aggregate.Min;
2525
import org.elasticsearch.xpack.sql.expression.function.aggregate.TopHits;
26+
import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunction;
2627
import org.elasticsearch.xpack.sql.expression.function.grouping.GroupingFunctionAttribute;
2728
import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction;
2829
import org.elasticsearch.xpack.sql.expression.predicate.conditional.ConditionalFunction;
@@ -229,6 +230,7 @@ Collection<Failure> verify(LogicalPlan plan) {
229230
validateInExpression(p, localFailures);
230231
validateConditional(p, localFailures);
231232

233+
checkGroupingFunctionInGroupBy(p, localFailures);
232234
checkFilterOnAggs(p, localFailures);
233235
checkFilterOnGrouping(p, localFailures);
234236

@@ -586,6 +588,24 @@ private static boolean checkGroupMatch(Expression e, Node<?> source, List<Expres
586588
}
587589
return false;
588590
}
591+
592+
private static void checkGroupingFunctionInGroupBy(LogicalPlan p, Set<Failure> localFailures) {
593+
// check if the query has a grouping function (Histogram) but no GROUP BY
594+
if (p instanceof Project) {
595+
Project proj = (Project) p;
596+
proj.projections().forEach(e -> e.forEachDown(f ->
597+
localFailures.add(fail(f, "[{}] needs to be part of the grouping", Expressions.name(f))), GroupingFunction.class));
598+
} else if (p instanceof Aggregate) {
599+
// if it does have a GROUP BY, check if the groupings contain the grouping functions (Histograms)
600+
Aggregate a = (Aggregate) p;
601+
a.aggregates().forEach(agg -> agg.forEachDown(e -> {
602+
if (a.groupings().size() == 0
603+
|| Expressions.anyMatch(a.groupings(), g -> g instanceof Function && e.functionEquals((Function) g)) == false) {
604+
localFailures.add(fail(e, "[{}] needs to be part of the grouping", Expressions.name(e)));
605+
}
606+
}, GroupingFunction.class));
607+
}
608+
}
589609

590610
private static void checkFilterOnAggs(LogicalPlan p, Set<Failure> localFailures) {
591611
if (p instanceof Filter) {

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/grouping/GroupingFunction.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,6 @@ public GroupingFunctionAttribute toAttribute() {
5757
return lazyAttribute;
5858
}
5959

60-
@Override
61-
public final GroupingFunction replaceChildren(List<Expression> newChildren) {
62-
if (newChildren.size() != 1) {
63-
throw new IllegalArgumentException("expected [1] child but received [" + newChildren.size() + "]");
64-
}
65-
return replaceChild(newChildren.get(0));
66-
}
67-
68-
protected abstract GroupingFunction replaceChild(Expression newChild);
69-
7060
@Override
7161
protected Pipe makePipe() {
7262
// unresolved AggNameInput (should always get replaced by the folder)

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/grouping/Histogram.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
import org.elasticsearch.xpack.sql.expression.Expressions;
1111
import org.elasticsearch.xpack.sql.expression.Expressions.ParamOrdinal;
1212
import org.elasticsearch.xpack.sql.expression.Literal;
13-
import org.elasticsearch.xpack.sql.tree.Source;
1413
import org.elasticsearch.xpack.sql.tree.NodeInfo;
14+
import org.elasticsearch.xpack.sql.tree.Source;
1515
import org.elasticsearch.xpack.sql.type.DataType;
1616
import org.elasticsearch.xpack.sql.type.DataTypes;
1717

1818
import java.time.ZoneId;
19+
import java.util.Collections;
20+
import java.util.List;
1921
import java.util.Objects;
2022

2123
public class Histogram extends GroupingFunction {
@@ -24,8 +26,8 @@ public class Histogram extends GroupingFunction {
2426
private final ZoneId zoneId;
2527

2628
public Histogram(Source source, Expression field, Expression interval, ZoneId zoneId) {
27-
super(source, field);
28-
this.interval = (Literal) interval;
29+
super(source, field, Collections.singletonList(interval));
30+
this.interval = Literal.of(interval);
2931
this.zoneId = zoneId;
3032
}
3133

@@ -51,10 +53,13 @@ protected TypeResolution resolveType() {
5153

5254
return resolution;
5355
}
54-
56+
5557
@Override
56-
protected GroupingFunction replaceChild(Expression newChild) {
57-
return new Histogram(source(), newChild, interval, zoneId);
58+
public final GroupingFunction replaceChildren(List<Expression> newChildren) {
59+
if (newChildren.size() != 2) {
60+
throw new IllegalArgumentException("expected [2] children but received [" + newChildren.size() + "]");
61+
}
62+
return new Histogram(source(), newChildren.get(0), newChildren.get(1), zoneId);
5863
}
5964

6065
@Override

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/analysis/analyzer/VerifierErrorMessagesTests.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,41 @@ public void testAggsInHistogram() {
539539
assertEquals("1:47: Cannot use an aggregate [MAX] for grouping",
540540
error("SELECT MAX(date) FROM test GROUP BY HISTOGRAM(MAX(int), 1)"));
541541
}
542+
543+
public void testHistogramNotInGrouping() {
544+
assertEquals("1:8: [HISTOGRAM(date, INTERVAL 1 MONTH)] needs to be part of the grouping",
545+
error("SELECT HISTOGRAM(date, INTERVAL 1 MONTH) AS h FROM test"));
546+
}
547+
548+
public void testHistogramNotInGroupingWithCount() {
549+
assertEquals("1:8: [HISTOGRAM(date, INTERVAL 1 MONTH)] needs to be part of the grouping",
550+
error("SELECT HISTOGRAM(date, INTERVAL 1 MONTH) AS h, COUNT(*) FROM test"));
551+
}
552+
553+
public void testHistogramNotInGroupingWithMaxFirst() {
554+
assertEquals("1:19: [HISTOGRAM(date, INTERVAL 1 MONTH)] needs to be part of the grouping",
555+
error("SELECT MAX(date), HISTOGRAM(date, INTERVAL 1 MONTH) AS h FROM test"));
556+
}
557+
558+
public void testHistogramWithoutAliasNotInGrouping() {
559+
assertEquals("1:8: [HISTOGRAM(date, INTERVAL 1 MONTH)] needs to be part of the grouping",
560+
error("SELECT HISTOGRAM(date, INTERVAL 1 MONTH) FROM test"));
561+
}
562+
563+
public void testTwoHistogramsNotInGrouping() {
564+
assertEquals("1:48: [HISTOGRAM(date, INTERVAL 1 DAY)] needs to be part of the grouping",
565+
error("SELECT HISTOGRAM(date, INTERVAL 1 MONTH) AS h, HISTOGRAM(date, INTERVAL 1 DAY) FROM test GROUP BY h"));
566+
}
567+
568+
public void testHistogramNotInGrouping_WithGroupByField() {
569+
assertEquals("1:8: [HISTOGRAM(date, INTERVAL 1 MONTH)] needs to be part of the grouping",
570+
error("SELECT HISTOGRAM(date, INTERVAL 1 MONTH) FROM test GROUP BY date"));
571+
}
572+
573+
public void testScalarOfHistogramNotInGrouping() {
574+
assertEquals("1:14: [HISTOGRAM(date, INTERVAL 1 MONTH)] needs to be part of the grouping",
575+
error("SELECT MONTH(HISTOGRAM(date, INTERVAL 1 MONTH)) FROM test"));
576+
}
542577

543578
public void testErrorMessageForPercentileWithSecondArgBasedOnAField() {
544579
assertEquals("1:8: Second argument of PERCENTILE must be a constant, received [ABS(int)]",

0 commit comments

Comments
 (0)