Skip to content
This repository was archived by the owner on Aug 2, 2022. It is now read-only.

Commit 89ffd1a

Browse files
authored
Support table alias in new engine (#695)
* Change grammar and ast builder * Define index symbol and resolve it when visiting qualified name * Raise error if qualifier is not index name or alias * Fix index not found exception by adding another get name or alias method * Remove qualifier if select item is a qualified field * Prepare PR * Prepare PR * Add doctest * Add bug fix test * Add more UT for where after where PR merged
1 parent af3f9f1 commit 89ffd1a

File tree

20 files changed

+483
-32
lines changed

20 files changed

+483
-32
lines changed

core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/Analyzer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
package com.amazon.opendistroforelasticsearch.sql.analysis;
1717

18+
import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRUCT;
19+
1820
import com.amazon.opendistroforelasticsearch.sql.analysis.symbol.Namespace;
1921
import com.amazon.opendistroforelasticsearch.sql.analysis.symbol.Symbol;
2022
import com.amazon.opendistroforelasticsearch.sql.ast.AbstractNodeVisitor;
@@ -99,6 +101,11 @@ public LogicalPlan visitRelation(Relation node, AnalysisContext context) {
99101
TypeEnvironment curEnv = context.peek();
100102
Table table = storageEngine.getTable(node.getTableName());
101103
table.getFieldTypes().forEach((k, v) -> curEnv.define(new Symbol(Namespace.FIELD_NAME, k), v));
104+
105+
// Put index name or its alias in index namespace on type environment so qualifier
106+
// can be removed when analyzing qualified name. The value (expr type) here doesn't matter.
107+
curEnv.define(new Symbol(Namespace.INDEX_NAME, node.getTableNameOrAlias()), STRUCT);
108+
102109
return new LogicalRelation(node.getTableName());
103110
}
104111

core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzer.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,8 @@ public Expression visitField(Field node, AnalysisContext context) {
163163

164164
@Override
165165
public Expression visitQualifiedName(QualifiedName node, AnalysisContext context) {
166-
// Name with qualifier (index.field, index_alias.field, object/nested.inner_field
167-
// text.keyword) is not supported for now
168-
if (node.getParts().size() > 1) {
169-
throw new SyntaxCheckException(String.format(
170-
"Qualified name [%s] is not supported yet", node));
171-
}
172-
return visitIdentifier(node.toString(), context);
166+
QualifierAnalyzer qualifierAnalyzer = new QualifierAnalyzer(context);
167+
return visitIdentifier(qualifierAnalyzer.unqualified(node), context);
173168
}
174169

175170
private Expression visitIdentifier(String ident, AnalysisContext context) {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*
15+
*/
16+
17+
package com.amazon.opendistroforelasticsearch.sql.analysis;
18+
19+
import com.amazon.opendistroforelasticsearch.sql.analysis.symbol.Namespace;
20+
import com.amazon.opendistroforelasticsearch.sql.analysis.symbol.Symbol;
21+
import com.amazon.opendistroforelasticsearch.sql.ast.expression.QualifiedName;
22+
import com.amazon.opendistroforelasticsearch.sql.common.antlr.SyntaxCheckException;
23+
import com.amazon.opendistroforelasticsearch.sql.exception.SemanticCheckException;
24+
import java.util.Arrays;
25+
import java.util.Optional;
26+
import lombok.RequiredArgsConstructor;
27+
28+
/**
29+
* Analyzer that analyzes qualifier(s) in a full field name.
30+
*/
31+
@RequiredArgsConstructor
32+
public class QualifierAnalyzer {
33+
34+
private final AnalysisContext context;
35+
36+
public String unqualified(String... parts) {
37+
return unqualified(QualifiedName.of(Arrays.asList(parts)));
38+
}
39+
40+
/**
41+
* Get unqualified name if its qualifier symbol found is in index namespace
42+
* on type environment. Unqualified name means name with qualifier removed.
43+
* For example, unqualified name of "accounts.age" or "acc.age" is "age".
44+
*
45+
* @return unqualified name if criteria met above, otherwise original name
46+
*/
47+
public String unqualified(QualifiedName fullName) {
48+
return isQualifierIndexOrAlias(fullName) ? fullName.rest().toString() : fullName.toString();
49+
}
50+
51+
private boolean isQualifierIndexOrAlias(QualifiedName fullName) {
52+
Optional<String> qualifier = fullName.first();
53+
if (qualifier.isPresent()) {
54+
resolveQualifierSymbol(fullName, qualifier.get());
55+
return true;
56+
}
57+
return false;
58+
}
59+
60+
private void resolveQualifierSymbol(QualifiedName fullName, String qualifier) {
61+
try {
62+
context.peek().resolve(new Symbol(Namespace.INDEX_NAME, qualifier));
63+
} catch (SemanticCheckException e) {
64+
// Throw syntax check intentionally to indicate fall back to old engine.
65+
// Need change to semantic check exception in future.
66+
throw new SyntaxCheckException(String.format(
67+
"The qualifier [%s] of qualified name [%s] must be an index name or its alias",
68+
qualifier, fullName));
69+
}
70+
}
71+
72+
}

core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/SelectExpressionAnalyzer.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.amazon.opendistroforelasticsearch.sql.ast.expression.Alias;
2323
import com.amazon.opendistroforelasticsearch.sql.ast.expression.AllFields;
2424
import com.amazon.opendistroforelasticsearch.sql.ast.expression.Field;
25+
import com.amazon.opendistroforelasticsearch.sql.ast.expression.QualifiedName;
2526
import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedExpression;
2627
import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType;
2728
import com.amazon.opendistroforelasticsearch.sql.expression.DSL;
@@ -63,7 +64,8 @@ public List<NamedExpression> visitField(Field node, AnalysisContext context) {
6364

6465
@Override
6566
public List<NamedExpression> visitAlias(Alias node, AnalysisContext context) {
66-
return Collections.singletonList(DSL.named(node.getName(),
67+
return Collections.singletonList(DSL.named(
68+
unqualifiedNameIfFieldOnly(node, context),
6769
node.getDelegated().accept(expressionAnalyzer, context),
6870
node.getAlias()));
6971
}
@@ -76,4 +78,23 @@ public List<NamedExpression> visitAllFields(AllFields node,
7678
return lookupAllFields.entrySet().stream().map(entry -> DSL.named(entry.getKey(),
7779
new ReferenceExpression(entry.getKey(), entry.getValue()))).collect(Collectors.toList());
7880
}
81+
82+
/**
83+
* Get unqualified name if select item is just a field. For example, suppose an index
84+
* named "accounts", return "age" for "SELECT accounts.age". But do nothing for expression
85+
* in "SELECT ABS(accounts.age)".
86+
* Note that an assumption is made implicitly that original name field in Alias must be
87+
* the same as the values in QualifiedName. This is true because AST builder does this.
88+
* Otherwise, what unqualified() returns will override Alias's name as NamedExpression's name
89+
* even though the QualifiedName doesn't have qualifier.
90+
*/
91+
private String unqualifiedNameIfFieldOnly(Alias node, AnalysisContext context) {
92+
UnresolvedExpression selectItem = node.getDelegated();
93+
if (selectItem instanceof QualifiedName) {
94+
QualifierAnalyzer qualifierAnalyzer = new QualifierAnalyzer(context);
95+
return qualifierAnalyzer.unqualified((QualifiedName) selectItem);
96+
}
97+
return node.getName();
98+
}
99+
79100
}

core/src/main/java/com/amazon/opendistroforelasticsearch/sql/analysis/symbol/Namespace.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*/
2121
public enum Namespace {
2222

23+
INDEX_NAME("Index"),
2324
FIELD_NAME("Field"),
2425
FUNCTION_NAME("Function");
2526

core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/dsl/AstDSL.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import com.amazon.opendistroforelasticsearch.sql.ast.expression.AggregateFunction;
1919
import com.amazon.opendistroforelasticsearch.sql.ast.expression.Alias;
20-
import com.amazon.opendistroforelasticsearch.sql.ast.expression.AllFields;
2120
import com.amazon.opendistroforelasticsearch.sql.ast.expression.And;
2221
import com.amazon.opendistroforelasticsearch.sql.ast.expression.Argument;
2322
import com.amazon.opendistroforelasticsearch.sql.ast.expression.Compare;
@@ -61,10 +60,14 @@ public static UnresolvedPlan filter(UnresolvedPlan input, UnresolvedExpression e
6160
return new Filter(expression).attach(input);
6261
}
6362

64-
public static UnresolvedPlan relation(String tableName) {
63+
public UnresolvedPlan relation(String tableName) {
6564
return new Relation(qualifiedName(tableName));
6665
}
6766

67+
public UnresolvedPlan relation(String tableName, String alias) {
68+
return new Relation(qualifiedName(tableName), alias);
69+
}
70+
6871
public static UnresolvedPlan project(UnresolvedPlan input, UnresolvedExpression... projectList) {
6972
return new Project(Arrays.asList(projectList)).attach(input);
7073
}

core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/expression/QualifiedName.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public static QualifiedName of(String first, String... rest) {
6060
return new QualifiedName(parts);
6161
}
6262

63-
private static QualifiedName of(Iterable<String> parts) {
63+
public static QualifiedName of(Iterable<String> parts) {
6464
return new QualifiedName(parts);
6565
}
6666

@@ -78,6 +78,34 @@ public String getSuffix() {
7878
return parts.get(parts.size() - 1);
7979
}
8080

81+
/**
82+
* Get first part of the qualified name.
83+
* @return first part
84+
*/
85+
public Optional<String> first() {
86+
if (parts.size() == 1) {
87+
return Optional.empty();
88+
}
89+
return Optional.of(parts.get(0));
90+
}
91+
92+
/**
93+
* Get rest parts of the qualified name. Assume that there must be remaining parts
94+
* so caller is responsible for the check (first() or size() must be called first).
95+
* For example:
96+
* {@code
97+
* QualifiedName name = ...
98+
* Optional<String> first = name.first();
99+
* if (first.isPresent()) {
100+
* name.rest() ...
101+
* }
102+
* }
103+
* @return rest part(s)
104+
*/
105+
public QualifiedName rest() {
106+
return QualifiedName.of(parts.subList(1, parts.size()));
107+
}
108+
81109
public String toString() {
82110
return String.join(".", this.parts);
83111
}

core/src/main/java/com/amazon/opendistroforelasticsearch/sql/ast/tree/Relation.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,43 @@
1919
import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedExpression;
2020
import com.google.common.collect.ImmutableList;
2121
import java.util.List;
22+
import lombok.AllArgsConstructor;
2223
import lombok.EqualsAndHashCode;
2324
import lombok.RequiredArgsConstructor;
2425
import lombok.ToString;
2526

2627
/**
2728
* Logical plan node of Relation, the interface for building the searching sources.
2829
*/
30+
@AllArgsConstructor
2931
@ToString
3032
@EqualsAndHashCode(callSuper = false)
3133
@RequiredArgsConstructor
3234
public class Relation extends UnresolvedPlan {
3335
private final UnresolvedExpression tableName;
3436

37+
/**
38+
* Optional alias name for the relation.
39+
*/
40+
private String alias;
41+
42+
/**
43+
* Get original table name. Unwrap and get name if table name expression
44+
* is actually an Alias.
45+
* @return table name
46+
*/
3547
public String getTableName() {
3648
return tableName.toString();
3749
}
3850

51+
/**
52+
* Get original table name or its alias if present in Alias.
53+
* @return table name or its alias
54+
*/
55+
public String getTableNameOrAlias() {
56+
return (alias == null) ? getTableName() : alias;
57+
}
58+
3959
@Override
4060
public List<UnresolvedPlan> getChild() {
4161
return ImmutableList.of();

core/src/test/java/com/amazon/opendistroforelasticsearch/sql/analysis/ExpressionAnalyzerTest.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.integerValue;
2121
import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.BOOLEAN;
2222
import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER;
23-
import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTERVAL;
23+
import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRUCT;
2424
import static org.junit.jupiter.api.Assertions.assertEquals;
2525
import static org.junit.jupiter.api.Assertions.assertThrows;
2626

27+
import com.amazon.opendistroforelasticsearch.sql.analysis.symbol.Namespace;
28+
import com.amazon.opendistroforelasticsearch.sql.analysis.symbol.Symbol;
2729
import com.amazon.opendistroforelasticsearch.sql.ast.dsl.AstDSL;
2830
import com.amazon.opendistroforelasticsearch.sql.ast.expression.DataType;
2931
import com.amazon.opendistroforelasticsearch.sql.ast.expression.UnresolvedExpression;
@@ -92,22 +94,31 @@ public void qualified_name() {
9294
}
9395

9496
@Test
95-
public void interval() {
97+
public void qualified_name_with_qualifier() {
98+
analysisContext.push();
99+
analysisContext.peek().define(new Symbol(Namespace.INDEX_NAME, "index_alias"), STRUCT);
96100
assertAnalyzeEqual(
97-
dsl.interval(DSL.literal(1L), DSL.literal("DAY")),
98-
AstDSL.intervalLiteral(1L, DataType.LONG, "DAY"));
99-
}
101+
DSL.ref("integer_value", INTEGER),
102+
AstDSL.qualifiedName("index_alias", "integer_value")
103+
);
100104

101-
@Test
102-
public void skip_identifier_with_qualifier() {
105+
analysisContext.peek().define(new Symbol(Namespace.FIELD_NAME, "nested_field"), STRUCT);
103106
SyntaxCheckException exception =
104107
assertThrows(SyntaxCheckException.class,
105-
() -> analyze(AstDSL.qualifiedName("index_alias", "integer_value")));
106-
108+
() -> analyze(AstDSL.qualifiedName("nested_field", "integer_value")));
107109
assertEquals(
108-
"Qualified name [index_alias.integer_value] is not supported yet",
110+
"The qualifier [nested_field] of qualified name [nested_field.integer_value] "
111+
+ "must be an index name or its alias",
109112
exception.getMessage()
110113
);
114+
analysisContext.pop();
115+
}
116+
117+
@Test
118+
public void interval() {
119+
assertAnalyzeEqual(
120+
dsl.interval(DSL.literal(1L), DSL.literal("DAY")),
121+
AstDSL.intervalLiteral(1L, DataType.LONG, "DAY"));
111122
}
112123

113124
@Test

0 commit comments

Comments
 (0)