Skip to content

Commit 2a53124

Browse files
author
Andras Palinkas
committed
SQL: Fix incorrect parameter resolution (elastic#63710)
* SQL: Fix incorrect parameter resolution Summary of the issue and the root cause: ``` (1) SELECT 100, 100 -> success (2) SELECT ?, ? (with params: 100, 100) -> success (3) SELECT 100, 100 FROM test -> Unknown output attribute exception for the second 100 (4) SELECT ?, ? FROM test (params: 100, 100) -> Unknown output attribute exception for the second ? (5) SELECT field1 as "x", field1 as "x" FROM test -> Unknown output attribute exception for the second "x" ``` There are two separate issues at play here: 1. Construction of `AttributeMap`s keeps only one of the `Attribute`s with the same name even if the `id`s are different (see the `AttributeMapTests` in this PR). This should be fixed no matter what, we should not overwrite attributes with one another during the construction of the `AttributeMap`. 2. The `id` on the `Alias`es is not the same in case the `Alias`es have the same `name` and same `child` It was considered to simpy fix the second issue by just reassigning the same `id`s to the `Alias`es with the same name and child, but it would not solve the `unknown output attribute exception` (see notes below). This PR covers the fix for the first issue. Fix elastic#56013
1 parent da0a969 commit 2a53124

File tree

7 files changed

+233
-7
lines changed

7 files changed

+233
-7
lines changed

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ public AttributeMap() {
157157
delegate = new LinkedHashMap<>();
158158
}
159159

160+
/**
161+
* Please use the {@link AttributeMap#builder()} instead.
162+
*/
163+
@Deprecated
160164
public AttributeMap(Map<Attribute, E> attr) {
161165
if (attr.isEmpty()) {
162166
delegate = emptyMap();
@@ -368,4 +372,28 @@ public boolean equals(Object obj) {
368372
public String toString() {
369373
return delegate.toString();
370374
}
375+
376+
public static <E> Builder<E> builder() {
377+
return new Builder<>();
378+
}
379+
380+
public static class Builder<E> {
381+
private final AttributeMap<E> map = new AttributeMap<>();
382+
383+
private Builder() {}
384+
385+
public Builder<E> put(Attribute attr, E value) {
386+
map.add(attr, value);
387+
return this;
388+
}
389+
390+
public Builder<E> putAll(AttributeMap<E> m) {
391+
map.addAll(m);
392+
return this;
393+
}
394+
395+
public AttributeMap<E> build() {
396+
return new AttributeMap<>(map);
397+
}
398+
}
371399
}

x-pack/plugin/ql/src/test/java/org/elasticsearch/xpack/ql/expression/AttributeMapTests.java

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

88
import org.elasticsearch.test.ESTestCase;
99
import org.elasticsearch.xpack.ql.tree.Source;
10+
import org.elasticsearch.xpack.ql.type.DataTypes;
1011

1112
import java.util.Collection;
1213
import java.util.LinkedHashMap;
@@ -37,6 +38,47 @@ private static AttributeMap<String> threeMap() {
3738
return new AttributeMap<>(map);
3839
}
3940

41+
public void testAttributeMapWithSameAliasesCanResolveAttributes() {
42+
Alias param1 = createIntParameterAlias(1, 100);
43+
Alias param2 = createIntParameterAlias(2, 100);
44+
assertTrue(param1.equals(param2));
45+
assertTrue(param1.semanticEquals(param2));
46+
// equality on literals
47+
assertTrue(param1.child().equals(param2.child()));
48+
assertTrue(param1.child().semanticEquals(param2.child()));
49+
assertTrue(param1.toAttribute().equals(param2.toAttribute()));
50+
assertFalse(param1.toAttribute().semanticEquals(param2.toAttribute()));
51+
52+
Map<Attribute, Expression> collectRefs = new LinkedHashMap<>();
53+
for (Alias a : List.of(param1, param2)) {
54+
collectRefs.put(a.toAttribute(), a.child());
55+
}
56+
// we can look up the same item by both attributes
57+
assertNotNull(collectRefs.get(param1.toAttribute()));
58+
assertNotNull(collectRefs.get(param2.toAttribute()));
59+
AttributeMap<Expression> attributeMap = new AttributeMap<>(collectRefs);
60+
61+
// validate that all Alias can be e
62+
assertTrue(attributeMap.containsKey(param1.toAttribute()));
63+
assertFalse(attributeMap.containsKey(param2.toAttribute())); // results in unknown attribute exception
64+
65+
AttributeMap.Builder<Expression> mapBuilder = AttributeMap.builder();
66+
for (Alias a : List.of(param1, param2)) {
67+
mapBuilder.put(a.toAttribute(), a.child());
68+
}
69+
AttributeMap<Expression> newAttributeMap = mapBuilder.build();
70+
71+
assertTrue(newAttributeMap.containsKey(param1.toAttribute()));
72+
assertTrue(newAttributeMap.containsKey(param2.toAttribute())); // no more unknown attribute exception
73+
}
74+
75+
private Alias createIntParameterAlias(int index, int value) {
76+
Source source = new Source(1, index * 5, "?");
77+
Literal literal = new Literal(source, value, DataTypes.INTEGER);
78+
Alias alias = new Alias(literal.source(), literal.source().text(), literal);
79+
return alias;
80+
}
81+
4082
public void testEmptyConstructor() {
4183
AttributeMap<Object> m = new AttributeMap<>();
4284
assertThat(m.size(), is(0));

x-pack/plugin/sql/qa/server/src/main/resources/select.sql-spec

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ SELECT salary, first_name, salary AS x, salary y FROM test_emp ORDER BY y LIMIT
4848

4949
constantWithLimit
5050
SELECT 3 FROM "test_emp" LIMIT 5;
51+
sameConstantsWithLimit
52+
SELECT 3, 3, 5 FROM "test_emp" LIMIT 5;
53+
sameConstantsWithLimitV2
54+
SELECT 5, 3, 3 FROM "test_emp" LIMIT 5;
55+
sameConstantsWithLimitV3
56+
SELECT 3, 5, 3, 3 FROM "test_emp" LIMIT 5;
5157
constantAndColumnWithLimit
5258
SELECT 3, first_name, last_name FROM "test_emp" ORDER BY emp_no LIMIT 5;
5359
constantComparisonWithLimit

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ Collection<Failure> verify(LogicalPlan plan) {
179179

180180
if (failures.isEmpty()) {
181181
Set<Failure> localFailures = new LinkedHashSet<>();
182-
final Map<Attribute, Expression> collectRefs = new LinkedHashMap<>();
182+
AttributeMap.Builder<Expression> collectRefs = AttributeMap.builder();
183183

184184
checkFullTextSearchInSelect(plan, localFailures);
185185

@@ -193,7 +193,7 @@ Collection<Failure> verify(LogicalPlan plan) {
193193
}
194194
}));
195195

196-
AttributeMap<Expression> attributeRefs = new AttributeMap<>(collectRefs);
196+
AttributeMap<Expression> attributeRefs = collectRefs.build();
197197

198198
// for filtering out duplicated errors
199199
final Set<LogicalPlan> groupingFailures = new LinkedHashSet<>();

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/planner/QueryFolder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ protected PhysicalPlan rule(ProjectExec project) {
142142
EsQueryExec exec = (EsQueryExec) project.child();
143143
QueryContainer queryC = exec.queryContainer();
144144

145-
Map<Attribute, Expression> aliases = new LinkedHashMap<>(queryC.aliases());
146-
Map<Attribute, Pipe> processors = new LinkedHashMap<>(queryC.scalarFunctions());
145+
AttributeMap.Builder<Expression> aliases = AttributeMap.<Expression>builder().putAll(queryC.aliases());
146+
AttributeMap.Builder<Pipe> processors = AttributeMap.<Pipe>builder().putAll(queryC.scalarFunctions());
147147

148148
for (NamedExpression pj : project.projections()) {
149149
if (pj instanceof Alias) {
@@ -161,9 +161,9 @@ protected PhysicalPlan rule(ProjectExec project) {
161161
}
162162

163163
QueryContainer clone = new QueryContainer(queryC.query(), queryC.aggs(), queryC.fields(),
164-
new AttributeMap<>(aliases),
164+
aliases.build(),
165165
queryC.pseudoFunctions(),
166-
new AttributeMap<>(processors),
166+
processors.build(),
167167
queryC.sort(),
168168
queryC.limit(),
169169
queryC.shouldTrackHits(),
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.sql.parser;
8+
9+
import org.elasticsearch.test.ESTestCase;
10+
import org.elasticsearch.xpack.ql.expression.Literal;
11+
import org.elasticsearch.xpack.ql.expression.NamedExpression;
12+
import org.elasticsearch.xpack.ql.expression.UnresolvedAlias;
13+
import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.LessThan;
14+
import org.elasticsearch.xpack.ql.plan.logical.Filter;
15+
import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan;
16+
import org.elasticsearch.xpack.ql.plan.logical.Project;
17+
import org.elasticsearch.xpack.sql.proto.SqlTypedParamValue;
18+
19+
import java.util.List;
20+
21+
import static org.elasticsearch.xpack.ql.type.DateUtils.UTC;
22+
import static org.hamcrest.Matchers.equalTo;
23+
import static org.hamcrest.Matchers.everyItem;
24+
import static org.hamcrest.Matchers.instanceOf;
25+
import static org.hamcrest.Matchers.startsWith;
26+
27+
public class ParamLiteralTests extends ESTestCase {
28+
29+
private final SqlParser parser = new SqlParser();
30+
31+
private LogicalPlan parse(String sql, SqlTypedParamValue... parameters) {
32+
return parser.createStatement(sql, List.of(parameters), UTC);
33+
}
34+
35+
public void testMultipleParamLiteralsWithUnresolvedAliases() {
36+
LogicalPlan logicalPlan = parse("SELECT ?, ? FROM test",
37+
new SqlTypedParamValue("integer", 100),
38+
new SqlTypedParamValue("integer", 200)
39+
);
40+
List<? extends NamedExpression> projections = ((Project) logicalPlan.children().get(0)).projections();
41+
assertThat(projections, everyItem(instanceOf(UnresolvedAlias.class)));
42+
assertThat(projections.get(0).toString(), startsWith("100 AS ?"));
43+
assertThat(projections.get(1).toString(), startsWith("200 AS ?"));
44+
}
45+
46+
public void testMultipleParamLiteralsWithUnresolvedAliasesAndWhereClause() {
47+
LogicalPlan logicalPlan = parse("SELECT ?, ?, (?) FROM test WHERE 1 < ?",
48+
new SqlTypedParamValue("integer", 100),
49+
new SqlTypedParamValue("integer", 100),
50+
new SqlTypedParamValue("integer", 200),
51+
new SqlTypedParamValue("integer", 300)
52+
);
53+
Project project = (Project) logicalPlan.children().get(0);
54+
List<? extends NamedExpression> projections = project.projections();
55+
assertThat(projections, everyItem(instanceOf(UnresolvedAlias.class)));
56+
assertThat(projections.get(0).toString(), startsWith("100 AS ?"));
57+
assertThat(projections.get(1).toString(), startsWith("100 AS ?"));
58+
assertThat(projections.get(2).toString(), startsWith("200 AS ?"));
59+
assertThat(project.children().get(0), instanceOf(Filter.class));
60+
Filter filter = (Filter) project.children().get(0);
61+
assertThat(filter.condition(), instanceOf(LessThan.class));
62+
LessThan condition = (LessThan) filter.condition();
63+
assertThat(condition.left(), instanceOf(Literal.class));
64+
assertThat(condition.right(), instanceOf(Literal.class));
65+
assertThat(((Literal)condition.right()).value(), equalTo(300));
66+
}
67+
68+
public void testParamLiteralsWithUnresolvedAliasesAndMixedTypes() {
69+
LogicalPlan logicalPlan = parse("SELECT ?, ? FROM test",
70+
new SqlTypedParamValue("integer", 100),
71+
new SqlTypedParamValue("text", "100")
72+
);
73+
List<? extends NamedExpression> projections = ((Project) logicalPlan.children().get(0)).projections();
74+
assertThat(projections, everyItem(instanceOf(UnresolvedAlias.class)));
75+
assertThat(projections.get(0).toString(), startsWith("100 AS ?"));
76+
assertThat(projections.get(1).toString(), startsWith("100 AS ?"));
77+
}
78+
79+
public void testParamLiteralsWithResolvedAndUnresolvedAliases() {
80+
LogicalPlan logicalPlan = parse("SELECT ?, ? as x, ? FROM test",
81+
new SqlTypedParamValue("integer", 100),
82+
new SqlTypedParamValue("integer", 200),
83+
new SqlTypedParamValue("integer", 300)
84+
);
85+
List<? extends NamedExpression> projections = ((Project) logicalPlan.children().get(0)).projections();
86+
assertThat(projections.get(0).toString(), startsWith("100 AS ?"));
87+
assertThat(projections.get(1).toString(), startsWith("200 AS x#"));;
88+
assertThat(projections.get(2).toString(), startsWith("300 AS ?"));;
89+
}
90+
91+
}

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/planner/QueryTranslatorTests.java

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.elasticsearch.xpack.ql.expression.Expression;
2020
import org.elasticsearch.xpack.ql.expression.FieldAttribute;
2121
import org.elasticsearch.xpack.ql.expression.Literal;
22+
import org.elasticsearch.xpack.ql.expression.ReferenceAttribute;
2223
import org.elasticsearch.xpack.ql.expression.function.FunctionDefinition;
2324
import org.elasticsearch.xpack.ql.expression.function.aggregate.AggregateFunction;
2425
import org.elasticsearch.xpack.ql.expression.function.aggregate.Count;
@@ -67,6 +68,7 @@
6768
import org.elasticsearch.xpack.sql.plan.physical.PhysicalPlan;
6869
import org.elasticsearch.xpack.sql.planner.QueryFolder.FoldAggregate.GroupingContext;
6970
import org.elasticsearch.xpack.sql.planner.QueryTranslator.QueryTranslation;
71+
import org.elasticsearch.xpack.sql.proto.SqlTypedParamValue;
7072
import org.elasticsearch.xpack.sql.querydsl.agg.AggFilter;
7173
import org.elasticsearch.xpack.sql.querydsl.agg.GroupByDateHistogram;
7274
import org.elasticsearch.xpack.sql.querydsl.container.MetricAggRef;
@@ -99,6 +101,7 @@
99101
import static org.elasticsearch.xpack.sql.util.DateUtils.UTC;
100102
import static org.hamcrest.CoreMatchers.containsString;
101103
import static org.hamcrest.Matchers.endsWith;
104+
import static org.hamcrest.Matchers.everyItem;
102105
import static org.hamcrest.Matchers.instanceOf;
103106
import static org.hamcrest.Matchers.startsWith;
104107

@@ -132,7 +135,11 @@ private LogicalPlan plan(String sql, ZoneId zoneId) {
132135
}
133136

134137
private PhysicalPlan optimizeAndPlan(String sql) {
135-
return planner.plan(optimizer.optimize(plan(sql)), true);
138+
return optimizeAndPlan(plan(sql));
139+
}
140+
141+
private PhysicalPlan optimizeAndPlan(LogicalPlan plan) {
142+
return planner.plan(optimizer.optimize(plan),true);
136143
}
137144

138145
private QueryTranslation translate(Expression condition) {
@@ -143,6 +150,10 @@ private QueryTranslation translateWithAggs(Expression condition) {
143150
return QueryTranslator.toQuery(condition, true);
144151
}
145152

153+
private LogicalPlan parameterizedSql(String sql, SqlTypedParamValue... params) {
154+
return analyzer.analyze(parser.createStatement(sql, Arrays.asList(params), org.elasticsearch.xpack.ql.type.DateUtils.UTC), true);
155+
}
156+
146157
public void testTermEqualityAnalyzer() {
147158
LogicalPlan p = plan("SELECT some.string FROM test WHERE some.string = 'value'");
148159
assertTrue(p instanceof Project);
@@ -2206,4 +2217,52 @@ public void testScriptsInsideAggregateFunctions_WithDateField_AndExtendedStats()
22062217
+ "InternalSqlScriptUtils.asDateTime(params.a0),InternalSqlScriptUtils.asDateTime(params.v0)))\",\"lang\":\"painless\","
22072218
+ "\"params\":{\"v0\":\"2020-05-03T00:00:00.000Z\"}},\"gap_policy\":\"skip\"}}}}}}"));
22082219
}
2220+
2221+
public void testFoldingWithParamsWithoutIndex() {
2222+
PhysicalPlan p = optimizeAndPlan(parameterizedSql("SELECT ?, ?, ? FROM test",
2223+
new SqlTypedParamValue("integer", 100),
2224+
new SqlTypedParamValue("integer", 100),
2225+
new SqlTypedParamValue("integer", 200)));
2226+
assertThat(p.output(), everyItem(instanceOf(ReferenceAttribute.class)));
2227+
assertThat(p.output().get(0).toString(), startsWith("?{r}#"));
2228+
assertThat(p.output().get(1).toString(), startsWith("?{r}#"));
2229+
assertThat(p.output().get(2).toString(), startsWith("?{r}#"));
2230+
assertNotEquals(p.output().get(1).id(), p.output().get(2).id());
2231+
}
2232+
2233+
public void testSameAliasForParamAndField() {
2234+
PhysicalPlan p = optimizeAndPlan(parameterizedSql("SELECT ?, int as \"?\" FROM test",
2235+
new SqlTypedParamValue("integer", 100)));
2236+
assertThat(p.output(), everyItem(instanceOf(ReferenceAttribute.class)));
2237+
assertThat(p.output().get(0).toString(), startsWith("?{r}#"));
2238+
assertThat(p.output().get(1).toString(), startsWith("?{r}#"));
2239+
assertNotEquals(p.output().get(0).id(), p.output().get(1).id());
2240+
}
2241+
2242+
public void testSameAliasOnSameField() {
2243+
PhysicalPlan p = optimizeAndPlan(parameterizedSql("SELECT int as \"int\", int as \"int\" FROM test"));
2244+
assertThat(p.output(), everyItem(instanceOf(ReferenceAttribute.class)));
2245+
assertThat(p.output().get(0).toString(), startsWith("int{r}#"));
2246+
assertThat(p.output().get(1).toString(), startsWith("int{r}#"));
2247+
}
2248+
2249+
public void testFoldingWithMixedParamsWithoutAlias() {
2250+
PhysicalPlan p = optimizeAndPlan(parameterizedSql("SELECT ?, ? FROM test",
2251+
new SqlTypedParamValue("integer", 100),
2252+
new SqlTypedParamValue("text", "200")));
2253+
assertThat(p.output(), everyItem(instanceOf(ReferenceAttribute.class)));
2254+
assertThat(p.output().get(0).toString(), startsWith("?{r}#"));
2255+
assertThat(p.output().get(1).toString(), startsWith("?{r}#"));
2256+
}
2257+
2258+
public void testSameExpressionWithoutAlias() {
2259+
PhysicalPlan physicalPlan = optimizeAndPlan("SELECT 100, 100 FROM test");
2260+
assertEquals(EsQueryExec.class, physicalPlan.getClass());
2261+
EsQueryExec eqe = (EsQueryExec) physicalPlan;
2262+
assertEquals(2, eqe.output().size());
2263+
assertThat(eqe.output().get(0).toString(), startsWith("100{r}#"));
2264+
assertThat(eqe.output().get(1).toString(), startsWith("100{r}#"));
2265+
// these two should be semantically different reference attributes
2266+
assertNotEquals(eqe.output().get(0).id(), eqe.output().get(1).id());
2267+
}
22092268
}

0 commit comments

Comments
 (0)