Skip to content

Commit 794ee4f

Browse files
committed
SQL: Prevent grouping over grouping functions (#38649)
Improve verifier to disallow grouping over grouping functions (e.g. HISTOGRAM over HISTOGRAM). Close #38308 (cherry picked from commit 4e9b1cf)
1 parent 871036b commit 794ee4f

File tree

4 files changed

+34
-15
lines changed

4 files changed

+34
-15
lines changed

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -593,20 +593,36 @@ private static void checkGroupingFunctionInGroupBy(LogicalPlan p, Set<Failure> l
593593
// check if the query has a grouping function (Histogram) but no GROUP BY
594594
if (p instanceof Project) {
595595
Project proj = (Project) p;
596-
proj.projections().forEach(e -> e.forEachDown(f ->
596+
proj.projections().forEach(e -> e.forEachDown(f ->
597597
localFailures.add(fail(f, "[{}] needs to be part of the grouping", Expressions.name(f))), GroupingFunction.class));
598598
} else if (p instanceof Aggregate) {
599-
// if it does have a GROUP BY, check if the groupings contain the grouping functions (Histograms)
599+
// if it does have a GROUP BY, check if the groupings contain the grouping functions (Histograms)
600600
Aggregate a = (Aggregate) p;
601601
a.aggregates().forEach(agg -> agg.forEachDown(e -> {
602-
if (a.groupings().size() == 0
602+
if (a.groupings().size() == 0
603603
|| Expressions.anyMatch(a.groupings(), g -> g instanceof Function && e.functionEquals((Function) g)) == false) {
604604
localFailures.add(fail(e, "[{}] needs to be part of the grouping", Expressions.name(e)));
605605
}
606+
else {
607+
checkGroupingFunctionTarget(e, localFailures);
608+
}
609+
}, GroupingFunction.class));
610+
611+
a.groupings().forEach(g -> g.forEachDown(e -> {
612+
checkGroupingFunctionTarget(e, localFailures);
606613
}, GroupingFunction.class));
607614
}
608615
}
609616

617+
private static void checkGroupingFunctionTarget(GroupingFunction f, Set<Failure> localFailures) {
618+
f.field().forEachDown(e -> {
619+
if (e instanceof GroupingFunction) {
620+
localFailures.add(fail(f.field(), "Cannot embed grouping functions within each other, found [{}] in [{}]",
621+
Expressions.name(f.field()), Expressions.name(f)));
622+
}
623+
});
624+
}
625+
610626
private static void checkFilterOnAggs(LogicalPlan p, Set<Failure> localFailures) {
611627
if (p instanceof Filter) {
612628
Filter filter = (Filter) p;

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expression.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
import org.elasticsearch.xpack.sql.util.StringUtils;
1515

1616
import java.util.List;
17-
import java.util.Locale;
18-
19-
import static java.lang.String.format;
2017

2118
/**
2219
* In a SQL statement, an Expression is whatever a user specifies inside an
@@ -39,10 +36,6 @@ public TypeResolution(String message) {
3936
this(true, message);
4037
}
4138

42-
TypeResolution(String message, Object... args) {
43-
this(true, format(Locale.ROOT, message, args));
44-
}
45-
4639
private TypeResolution(boolean unresolved, String message) {
4740
this.failed = unresolved;
4841
this.message = message;

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/Expressions.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
import java.util.StringJoiner;
1919
import java.util.function.Predicate;
2020

21-
import static java.lang.String.format;
2221
import static java.util.Collections.emptyList;
2322
import static java.util.Collections.emptyMap;
23+
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
2424
import static org.elasticsearch.xpack.sql.type.DataType.BOOLEAN;
2525

2626
public final class Expressions {
@@ -186,7 +186,7 @@ public static TypeResolution typeMustBe(Expression e,
186186
String... acceptedTypes) {
187187
return predicate.test(e.dataType()) || DataTypes.isNull(e.dataType())?
188188
TypeResolution.TYPE_RESOLVED :
189-
new TypeResolution(format(Locale.ROOT, "[%s]%s argument must be [%s], found value [%s] type [%s]",
189+
new TypeResolution(format(null, "[{}]{} argument must be [{}], found value [{}] type [{}]",
190190
operationName,
191191
paramOrd == null || paramOrd == ParamOrdinal.DEFAULT ? "" : " " + paramOrd.name().toLowerCase(Locale.ROOT),
192192
acceptedTypesForErrorMsg(acceptedTypes),

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -566,10 +566,20 @@ public void testGroupByScalarOnTopOfGrouping() {
566566
}
567567

568568
public void testAggsInHistogram() {
569-
assertEquals("1:47: Cannot use an aggregate [MAX] for grouping",
570-
error("SELECT MAX(date) FROM test GROUP BY HISTOGRAM(MAX(int), 1)"));
569+
assertEquals("1:37: Cannot use an aggregate [MAX] for grouping",
570+
error("SELECT MAX(date) FROM test GROUP BY MAX(int)"));
571571
}
572-
572+
573+
public void testGroupingsInHistogram() {
574+
assertEquals(
575+
"1:47: Cannot embed grouping functions within each other, found [HISTOGRAM(int, 1)] in [HISTOGRAM(HISTOGRAM(int, 1), 1)]",
576+
error("SELECT MAX(date) FROM test GROUP BY HISTOGRAM(HISTOGRAM(int, 1), 1)"));
577+
}
578+
579+
public void testCastInHistogram() {
580+
accept("SELECT MAX(date) FROM test GROUP BY HISTOGRAM(CAST(int AS LONG), 1)");
581+
}
582+
573583
public void testHistogramNotInGrouping() {
574584
assertEquals("1:8: [HISTOGRAM(date, INTERVAL 1 MONTH)] needs to be part of the grouping",
575585
error("SELECT HISTOGRAM(date, INTERVAL 1 MONTH) AS h FROM test"));

0 commit comments

Comments
 (0)