Skip to content

Commit 9ba70a3

Browse files
bpinteaverkhovin
andauthored
SQL: fix NPE on ambiguous GROUP BY (#59370)
* fix npe on ambiguous group by * add tests for aggregates and group by, add quotes to error message * add more cases for Group By ambiguity test * change error messages for field ambiguity * change collection aliases approach * add locations of attributes for ambiguous grouping error * Adress review comments - remove Comparable implementations from Attribute and Location; - add ad-hoc comparator for sorting locations in ambiguity message; - remove added AttributeAlias class with Touple; - add code comment to explain issue with Location overwriting. * Fix c&p error in location ref generation comparator Fix copy&paste error in dedicated comparator used for sorting ambiguity location references. Slightly increase its readability. Co-authored-by: Nikita Verkhovin <[email protected]>
1 parent 406bad8 commit 9ba70a3

File tree

3 files changed

+114
-24
lines changed

3 files changed

+114
-24
lines changed

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package org.elasticsearch.xpack.ql.expression;
77

8+
import org.elasticsearch.common.collect.Tuple;
89
import org.elasticsearch.xpack.ql.QlIllegalArgumentException;
910
import org.elasticsearch.xpack.ql.expression.function.Function;
1011
import org.elasticsearch.xpack.ql.expression.gen.pipeline.AttributeInput;
@@ -14,10 +15,8 @@
1415

1516
import java.util.ArrayList;
1617
import java.util.Collection;
17-
import java.util.LinkedHashMap;
1818
import java.util.LinkedHashSet;
1919
import java.util.List;
20-
import java.util.Map;
2120
import java.util.Set;
2221
import java.util.function.Predicate;
2322

@@ -164,14 +163,15 @@ public static boolean equalsAsAttribute(Expression left, Expression right) {
164163
return true;
165164
}
166165

167-
public static AttributeMap<Expression> aliases(List<? extends NamedExpression> named) {
168-
Map<Attribute, Expression> aliasMap = new LinkedHashMap<>();
166+
public static List<Tuple<Attribute, Expression>> aliases(List<? extends NamedExpression> named) {
167+
// an alias of same name and data type can be reused (by mistake): need to use a list to collect all refs (and later report them)
168+
List<Tuple<Attribute, Expression>> aliases = new ArrayList<>();
169169
for (NamedExpression ne : named) {
170170
if (ne instanceof Alias) {
171-
aliasMap.put(ne.toAttribute(), ((Alias) ne).child());
171+
aliases.add(new Tuple<>(ne.toAttribute(), ((Alias) ne).child()));
172172
}
173173
}
174-
return new AttributeMap<>(aliasMap);
174+
return aliases;
175175
}
176176

177177
public static boolean hasReferenceAttribute(Collection<Attribute> output) {

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

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
*/
66
package org.elasticsearch.xpack.sql.analysis.analyzer;
77

8+
import org.elasticsearch.common.collect.Tuple;
89
import org.elasticsearch.common.logging.LoggerMessageFormat;
910
import org.elasticsearch.xpack.ql.capabilities.Resolvables;
1011
import org.elasticsearch.xpack.ql.common.Failure;
1112
import org.elasticsearch.xpack.ql.expression.Alias;
1213
import org.elasticsearch.xpack.ql.expression.Attribute;
13-
import org.elasticsearch.xpack.ql.expression.AttributeMap;
1414
import org.elasticsearch.xpack.ql.expression.AttributeSet;
1515
import org.elasticsearch.xpack.ql.expression.Expression;
1616
import org.elasticsearch.xpack.ql.expression.Expressions;
@@ -181,7 +181,7 @@ private static Attribute resolveAgainstList(UnresolvedAttribute u, Collection<At
181181
// but also if the qualifier might not be quoted and if there's any ambiguity with nested fields
182182
|| Objects.equals(u.name(), attribute.qualifiedName()));
183183
if (match) {
184-
matches.add(attribute.withLocation(u.source()));
184+
matches.add(attribute);
185185
}
186186
}
187187
}
@@ -192,16 +192,21 @@ private static Attribute resolveAgainstList(UnresolvedAttribute u, Collection<At
192192
}
193193

194194
if (matches.size() == 1) {
195-
return handleSpecialFields(u, matches.get(0), allowCompound);
195+
// only add the location if the match is univocal; b/c otherwise adding the location will overwrite any preexisting one
196+
return handleSpecialFields(u, matches.get(0).withLocation(u.source()), allowCompound);
196197
}
197198

198-
return u.withUnresolvedMessage("Reference [" + u.qualifiedName()
199-
+ "] is ambiguous (to disambiguate use quotes or qualifiers); matches any of " +
200-
matches.stream()
201-
.map(a -> "\"" + a.qualifier() + "\".\"" + a.name() + "\"")
202-
.sorted()
203-
.collect(toList())
204-
);
199+
List<String> refs = matches.stream()
200+
.sorted((a, b) -> {
201+
int lineDiff = a.sourceLocation().getLineNumber() - b.sourceLocation().getLineNumber();
202+
int colDiff = a.sourceLocation().getColumnNumber() - b.sourceLocation().getColumnNumber();
203+
return lineDiff != 0 ? lineDiff : (colDiff != 0 ? colDiff : a.qualifiedName().compareTo(b.qualifiedName()));
204+
})
205+
.map(a -> "line " + a.sourceLocation().toString().substring(1) + " [" +
206+
(a.qualifier() != null ? "\"" + a.qualifier() + "\".\"" + a.name() + "\"" : a.name()) + "]")
207+
.collect(toList());
208+
return u.withUnresolvedMessage("Reference [" + u.qualifiedName() + "] is ambiguous (to disambiguate use quotes or qualifiers); " +
209+
"matches any of " + refs);
205210
}
206211

207212
private static Attribute handleSpecialFields(UnresolvedAttribute u, Attribute named, boolean allowCompound) {
@@ -327,22 +332,31 @@ else if (plan instanceof Aggregate) {
327332
return new Aggregate(a.source(), a.child(), a.groupings(),
328333
expandProjections(a.aggregates(), a.child()));
329334
}
330-
// if the grouping is unresolved but the aggs are, use the former to resolve the latter
331-
// solves the case of queries declaring an alias in SELECT and referring to it in GROUP BY
335+
// if the grouping is unresolved but the aggs are, use the latter to resolve the former.
336+
// solves the case of queries declaring an alias in SELECT and referring to it in GROUP BY.
332337
// e.g. SELECT x AS a ... GROUP BY a
333338
if (!a.expressionsResolved() && Resolvables.resolved(a.aggregates())) {
334339
List<Expression> groupings = a.groupings();
335340
List<Expression> newGroupings = new ArrayList<>();
336-
AttributeMap<Expression> resolved = Expressions.aliases(a.aggregates());
341+
List<Tuple<Attribute, Expression>> resolvedAliases = Expressions.aliases(a.aggregates());
337342

338343
boolean changed = false;
339344
for (Expression grouping : groupings) {
340345
if (grouping instanceof UnresolvedAttribute) {
341-
Attribute maybeResolved = resolveAgainstList((UnresolvedAttribute) grouping, resolved.keySet());
346+
Attribute maybeResolved = resolveAgainstList((UnresolvedAttribute) grouping,
347+
resolvedAliases.stream().map(Tuple::v1).collect(toList()));
342348
if (maybeResolved != null) {
343349
changed = true;
344-
// use the matched expression (not its attribute)
345-
grouping = resolved.get(maybeResolved);
350+
if (maybeResolved.resolved()) {
351+
grouping = resolvedAliases.stream()
352+
.filter(t -> t.v1().equals(maybeResolved))
353+
// use the matched expression (not its attribute)
354+
.map(Tuple::v2)
355+
.findAny()
356+
.get(); // there should always be exactly one match
357+
} else {
358+
grouping = maybeResolved;
359+
}
346360
}
347361
}
348362
newGroupings.add(grouping);

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

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@
55
*/
66
package org.elasticsearch.xpack.sql.analysis.analyzer;
77

8+
import java.util.stream.Collectors;
89
import org.elasticsearch.test.ESTestCase;
910
import org.elasticsearch.xpack.ql.QlIllegalArgumentException;
11+
import org.elasticsearch.xpack.ql.expression.Alias;
1012
import org.elasticsearch.xpack.ql.expression.Attribute;
13+
import org.elasticsearch.xpack.ql.expression.Expression;
1114
import org.elasticsearch.xpack.ql.expression.Expressions;
1215
import org.elasticsearch.xpack.ql.expression.FieldAttribute;
1316
import org.elasticsearch.xpack.ql.expression.NamedExpression;
1417
import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry;
1518
import org.elasticsearch.xpack.ql.index.EsIndex;
1619
import org.elasticsearch.xpack.ql.index.IndexResolution;
20+
import org.elasticsearch.xpack.ql.plan.logical.Aggregate;
1721
import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan;
1822
import org.elasticsearch.xpack.ql.plan.logical.Project;
1923
import org.elasticsearch.xpack.ql.type.EsField;
@@ -31,6 +35,7 @@
3135
import static org.elasticsearch.xpack.ql.type.DataTypes.TEXT;
3236
import static org.elasticsearch.xpack.sql.types.SqlTypesTests.loadMapping;
3337
import static org.hamcrest.CoreMatchers.instanceOf;
38+
import static org.hamcrest.Matchers.contains;
3439
import static org.hamcrest.Matchers.hasItem;
3540
import static org.hamcrest.Matchers.hasItems;
3641
import static org.hamcrest.Matchers.hasSize;
@@ -178,13 +183,13 @@ public void testFieldAmbiguity() {
178183
VerificationException ex = expectThrows(VerificationException.class, () -> plan("SELECT test.bar FROM test"));
179184
assertEquals(
180185
"Found 1 problem\nline 1:8: Reference [test.bar] is ambiguous (to disambiguate use quotes or qualifiers); "
181-
+ "matches any of [\"test\".\"bar\", \"test\".\"test.bar\"]",
186+
+ "matches any of [line 1:22 [\"test\".\"bar\"], line 1:22 [\"test\".\"test.bar\"]]",
182187
ex.getMessage());
183188

184189
ex = expectThrows(VerificationException.class, () -> plan("SELECT test.test FROM test"));
185190
assertEquals(
186191
"Found 1 problem\nline 1:8: Reference [test.test] is ambiguous (to disambiguate use quotes or qualifiers); "
187-
+ "matches any of [\"test\".\"test\", \"test\".\"test.test\"]",
192+
+ "matches any of [line 1:23 [\"test\".\"test\"], line 1:23 [\"test\".\"test.test\"]]",
188193
ex.getMessage());
189194

190195
LogicalPlan plan = plan("SELECT test.test FROM test AS x");
@@ -201,4 +206,75 @@ public void testFieldAmbiguity() {
201206
assertThat(attribute.qualifier(), is("test"));
202207
assertThat(attribute.name(), is("test.test"));
203208
}
209+
210+
public void testAggregations() {
211+
Map<String, EsField> mapping = TypesTests.loadMapping("mapping-basic.json");
212+
EsIndex index = new EsIndex("test", mapping);
213+
getIndexResult = IndexResolution.valid(index);
214+
analyzer = new Analyzer(SqlTestUtils.TEST_CFG, functionRegistry, getIndexResult, verifier);
215+
216+
LogicalPlan plan = plan("SELECT sum(salary) AS s FROM test");
217+
assertThat(plan, instanceOf(Aggregate.class));
218+
219+
Aggregate aggregate = (Aggregate) plan;
220+
assertThat(aggregate.aggregates(), hasSize(1));
221+
NamedExpression attribute = aggregate.aggregates().get(0);
222+
assertThat(attribute, instanceOf(Alias.class));
223+
assertThat(attribute.name(), is("s"));
224+
assertThat(aggregate.groupings(), hasSize(0));
225+
226+
plan = plan("SELECT gender AS g, sum(salary) AS s FROM test GROUP BY g");
227+
assertThat(plan, instanceOf(Aggregate.class));
228+
229+
aggregate = (Aggregate) plan;
230+
List<? extends NamedExpression> aggregates = aggregate.aggregates();
231+
assertThat(aggregates, hasSize(2));
232+
assertThat(aggregates.get(0), instanceOf(Alias.class));
233+
assertThat(aggregates.get(1), instanceOf(Alias.class));
234+
List<String> names = aggregate.aggregates().stream().map(NamedExpression::name).collect(Collectors.toList());
235+
assertThat(names, contains("g", "s"));
236+
237+
List<Expression> groupings = aggregate.groupings();
238+
assertThat(groupings, hasSize(1));
239+
FieldAttribute grouping = (FieldAttribute) groupings.get(0);
240+
assertThat(grouping.name(), is("gender"));
241+
}
242+
243+
public void testGroupByAmbiguity() {
244+
Map<String, EsField> mapping = TypesTests.loadMapping("mapping-basic.json");
245+
EsIndex index = new EsIndex("test", mapping);
246+
getIndexResult = IndexResolution.valid(index);
247+
analyzer = new Analyzer(SqlTestUtils.TEST_CFG, functionRegistry, getIndexResult, verifier);
248+
249+
VerificationException ex = expectThrows(VerificationException.class,
250+
() -> plan("SELECT gender AS g, sum(salary) AS g FROM test GROUP BY g"));
251+
assertEquals(
252+
"Found 1 problem\nline 1:57: Reference [g] is ambiguous (to disambiguate use quotes or qualifiers); " +
253+
"matches any of [line 1:8 [g], line 1:21 [g]]",
254+
ex.getMessage());
255+
256+
ex = expectThrows(VerificationException.class,
257+
() -> plan("SELECT gender AS g, max(salary) AS g, min(salary) AS g FROM test GROUP BY g"));
258+
assertEquals(
259+
"Found 1 problem\nline 1:75: Reference [g] is ambiguous (to disambiguate use quotes or qualifiers); " +
260+
"matches any of [line 1:8 [g], line 1:21 [g], line 1:39 [g]]",
261+
ex.getMessage());
262+
263+
ex = expectThrows(VerificationException.class,
264+
() -> plan("SELECT gender AS g, last_name AS g, sum(salary) AS s FROM test GROUP BY g"));
265+
assertEquals(
266+
"Found 1 problem\nline 1:73: Reference [g] is ambiguous (to disambiguate use quotes or qualifiers); " +
267+
"matches any of [line 1:8 [g], line 1:21 [g]]",
268+
ex.getMessage());
269+
270+
ex = expectThrows(VerificationException.class,
271+
() -> plan("SELECT gender AS g, last_name AS g, min(salary) AS m, max(salary) as m FROM test GROUP BY g, m"));
272+
assertEquals(
273+
"Found 2 problems\n" +
274+
"line 1:91: Reference [g] is ambiguous (to disambiguate use quotes or qualifiers); "
275+
+ "matches any of [line 1:8 [g], line 1:21 [g]]\n" +
276+
"line 1:94: Reference [m] is ambiguous (to disambiguate use quotes or qualifiers); "
277+
+ "matches any of [line 1:37 [m], line 1:55 [m]]",
278+
ex.getMessage());
279+
}
204280
}

0 commit comments

Comments
 (0)