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

Commit a8ae9e0

Browse files
authored
Merge #584, #600, #609 for OD 1.8 (#658)
* Issue 584 Fix object/nested field select issue * Issue 600 Fix CAST bool to integer issue * Issue 609 Support queries end with semi colon
1 parent a9538cb commit a8ae9e0

File tree

18 files changed

+359
-41
lines changed

18 files changed

+359
-41
lines changed

src/main/antlr/OpenDistroSqlParser.g4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ options { tokenVocab=OpenDistroSqlLexer; }
3232

3333
// Root rule
3434
root
35-
: sqlStatement? EOF
35+
: sqlStatement? SEMI? EOF
3636
;
3737

3838
// Only SELECT, DELETE, SHOW and DSCRIBE are supported for now

src/main/java/com/amazon/opendistroforelasticsearch/sql/esdomain/mapping/FieldMapping.java

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.amazon.opendistroforelasticsearch.sql.esdomain.mapping;
1717

1818
import com.amazon.opendistroforelasticsearch.sql.domain.Field;
19+
import com.amazon.opendistroforelasticsearch.sql.executor.format.DescribeResultSet;
1920
import com.amazon.opendistroforelasticsearch.sql.utils.StringUtils;
2021

2122
import java.util.Map;
@@ -119,24 +120,26 @@ public String path() {
119120
}
120121

121122
/**
122-
* Used to retrieve the type of fields from metaData map structures for both regular and nested fields
123+
* Find field type in ES Get Field Mapping API response. Note that Get Field Mapping API does NOT return
124+
* the type for object or nested field. In this case, object type is used as default under the assumption
125+
* that the field queried here must exist (which is true if semantic analyzer is enabled).
126+
*
127+
* @return field type if found in mapping, otherwise "object" type returned
123128
*/
124129
@SuppressWarnings("unchecked")
125130
public String type() {
126-
FieldMappingMetaData metaData = typeMappings.get(fieldName);
131+
FieldMappingMetaData metaData = typeMappings.getOrDefault(fieldName, FieldMappingMetaData.NULL);
132+
if (metaData.isNull()) {
133+
return DescribeResultSet.DEFAULT_OBJECT_DATATYPE;
134+
}
135+
127136
Map<String, Object> source = metaData.sourceAsMap();
128137
String[] fieldPath = fieldName.split("\\.");
129138

130-
/*
131-
* When field is not nested the metaData source is fieldName -> type
132-
* When it is nested or contains "." in general (ex. fieldName.nestedName) the source is nestedName -> type
133-
*/
134-
String root = (fieldPath.length == 1) ? fieldName : fieldPath[1];
135-
Map<String, Object> fieldMapping = (Map<String, Object>) source.get(root);
136-
for (int i = 2; i < fieldPath.length; i++) {
137-
fieldMapping = (Map<String, Object>) fieldMapping.get(fieldPath[i]);
138-
}
139-
139+
// For object/nested field, fieldName is full path though only innermost field name present in mapping
140+
// For example, fieldName='employee.location.city', metaData='{"city":{"type":"text"}}'
141+
String innermostFieldName = (fieldPath.length == 1) ? fieldName : fieldPath[fieldPath.length - 1];
142+
Map<String, Object> fieldMapping = (Map<String, Object>) source.get(innermostFieldName);
140143
return (String) fieldMapping.get("type");
141144
}
142145

src/main/java/com/amazon/opendistroforelasticsearch/sql/executor/format/DescribeResultSet.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class DescribeResultSet extends ResultSet {
4040
* You are not required to set the field type to object explicitly, as this is the default value.
4141
* https://www.elastic.co/guide/en/elasticsearch/reference/current/object.html
4242
*/
43-
private static final String DEFAULT_OBJECT_DATATYPE = "object";
43+
public static final String DEFAULT_OBJECT_DATATYPE = "object";
4444

4545
private IndexStatement statement;
4646
private Object queryResult;

src/main/java/com/amazon/opendistroforelasticsearch/sql/executor/format/Schema.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ public enum Type {
114114
DATE, // Date types
115115
BOOLEAN, // Boolean types
116116
BINARY, // Binary types
117+
OBJECT,
118+
NESTED,
117119
INTEGER_RANGE, FLOAT_RANGE, LONG_RANGE, DOUBLE_RANGE, DATE_RANGE; // Range types
118120

119121
public String nameLowerCase() {

src/main/java/com/amazon/opendistroforelasticsearch/sql/executor/format/SelectResultSet.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ private List<Schema.Column> populateColumns(Query query, String[] fieldNames, Ma
437437
// _score is a special case since it is not included in typeMappings, so it is checked for here
438438
if (fieldName.equals(SCORE)) {
439439
columns.add(new Schema.Column(fieldName, fetchAlias(fieldName, fieldMap), Schema.Type.FLOAT));
440+
continue;
440441
}
441442
/*
442443
* Methods are also a special case as their type cannot be determined from typeMappings, so it is checked
@@ -465,6 +466,7 @@ private List<Schema.Column> populateColumns(Query query, String[] fieldNames, Ma
465466
fetchMethodReturnType(fieldIndex, methodField)
466467
)
467468
);
469+
continue;
468470
}
469471

470472
/*
@@ -473,7 +475,7 @@ private List<Schema.Column> populateColumns(Query query, String[] fieldNames, Ma
473475
* explicitly selected.
474476
*/
475477
FieldMapping field = new FieldMapping(fieldName, typeMappings, fieldMap);
476-
if (typeMappings.containsKey(fieldName) && !field.isMetaField()) {
478+
if (!field.isMetaField()) {
477479

478480
if (field.isMultiField() && !field.isSpecified()) {
479481
continue;

src/main/java/com/amazon/opendistroforelasticsearch/sql/query/ESActionFactory.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,11 @@ public static QueryAction create(Client client, String sql)
8787
public static QueryAction create(Client client, QueryActionRequest request)
8888
throws SqlParseException, SQLFeatureNotSupportedException {
8989
String sql = request.getSql();
90-
// Linebreak matcher
90+
// Remove line breaker anywhere and semicolon at the end
9191
sql = sql.replaceAll("\\R", " ").trim();
92+
if (sql.endsWith(";")) {
93+
sql = sql.substring(0, sql.length() - 1);
94+
}
9295

9396
switch (getFirstWord(sql)) {
9497
case "SELECT":

src/main/java/com/amazon/opendistroforelasticsearch/sql/utils/SQLFunctions.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -973,13 +973,10 @@ public String getCastScriptStatement(String name, String castType, List<KVValue>
973973
String castFieldName = String.format("doc['%s'].value", paramers.get(0).toString());
974974
switch (StringUtils.toUpper(castType)) {
975975
case "INT":
976-
return String.format("def %s = Double.parseDouble(%s.toString()).intValue()", name, castFieldName);
977976
case "LONG":
978-
return String.format("def %s = Double.parseDouble(%s.toString()).longValue()", name, castFieldName);
979977
case "FLOAT":
980-
return String.format("def %s = Double.parseDouble(%s.toString()).floatValue()", name, castFieldName);
981978
case "DOUBLE":
982-
return String.format("def %s = Double.parseDouble(%s.toString()).doubleValue()", name, castFieldName);
979+
return getCastToNumericValueScript(name, castFieldName, StringUtils.toLower(castType));
983980
case "STRING":
984981
return String.format("def %s = %s.toString()", name, castFieldName);
985982
case "DATETIME":
@@ -990,6 +987,14 @@ public String getCastScriptStatement(String name, String castType, List<KVValue>
990987
}
991988
}
992989

990+
private String getCastToNumericValueScript(String varName, String docValue, String targetType) {
991+
String script =
992+
"def %1$s = (%2$s instanceof boolean) "
993+
+ "? (%2$s ? 1 : 0) "
994+
+ ": Double.parseDouble(%2$s.toString()).%3$sValue()";
995+
return StringUtils.format(script, varName, docValue, targetType);
996+
}
997+
993998
/**
994999
* Returns return type of script function. This is simple approach, that might be not the best solution in the long
9951000
* term. For example - for JDBC, if the column type in index is INTEGER, and the query is "select column+5", current

src/test/java/com/amazon/opendistroforelasticsearch/sql/antlr/SyntaxAnalysisTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void missingWhereKeywordShouldThrowException() {
7979
expectValidationFailWithErrorMessage(
8080
"SELECT * FROM accounts age = 1",
8181
"offending symbol [=]", // parser thought 'age' is alias of 'accounts' and failed at '='
82-
"Expecting", "'WHERE'" // "Expecting tokens in {<EOF>, 'INNER', 'JOIN', ... 'WHERE', ','}"
82+
"Expecting", ";" // "Expecting tokens in {<EOF>, ';'}"
8383
);
8484
}
8585

@@ -130,6 +130,11 @@ public void arithmeticExpressionInWhereClauseShouldPass() {
130130
validate("SELECT * FROM accounts WHERE age + 1 = 10");
131131
}
132132

133+
@Test
134+
public void queryEndWithSemiColonShouldPass() {
135+
validate("SELECT * FROM accounts;");
136+
}
137+
133138
private void expectValidationFailWithErrorMessage(String query, String... messages) {
134139
exception.expect(SyntaxAnalysisException.class);
135140
exception.expectMessage(allOf(Arrays.stream(messages).

src/test/java/com/amazon/opendistroforelasticsearch/sql/esdomain/mapping/FieldMappingTest.java

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@
1515

1616
package com.amazon.opendistroforelasticsearch.sql.esdomain.mapping;
1717

18+
import static java.util.Collections.emptyMap;
19+
import static org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsResponse.FieldMappingMetaData;
20+
import static org.hamcrest.Matchers.is;
21+
import static org.junit.Assert.assertThat;
22+
1823
import com.amazon.opendistroforelasticsearch.sql.domain.Field;
1924
import com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils;
20-
import org.hamcrest.Matcher;
21-
import org.junit.Test;
22-
25+
import com.google.common.collect.ImmutableMap;
2326
import java.util.Arrays;
2427
import java.util.Map;
2528
import java.util.stream.Collectors;
26-
27-
import static java.util.Collections.emptyMap;
28-
import static org.hamcrest.Matchers.is;
29-
import static org.junit.Assert.assertThat;
29+
import org.elasticsearch.common.bytes.BytesArray;
30+
import org.hamcrest.Matcher;
31+
import org.junit.Test;
3032

3133
/**
3234
* Unit test for {@code FieldMapping} with trivial methods ignored such as isSpecified, isMetaField etc.
@@ -81,6 +83,35 @@ public void testMultiFieldIsNotProperty() {
8183
);
8284
}
8385

86+
@Test
87+
public void testUnknownFieldTreatedAsObject() {
88+
assertThat(
89+
new FieldMapping("employee"),
90+
hasType("object")
91+
);
92+
}
93+
94+
@Test
95+
public void testDeepNestedField() {
96+
assertThat(
97+
new FieldMapping(
98+
"employee.location.city",
99+
ImmutableMap.of(
100+
"employee.location.city",
101+
new FieldMappingMetaData("employee.location.city", new BytesArray(
102+
"{\n" +
103+
" \"city\" : {\n" +
104+
" \"type\" : \"text\"\n" +
105+
" }\n" +
106+
"}")
107+
)
108+
),
109+
emptyMap()
110+
),
111+
hasType("text")
112+
);
113+
}
114+
84115
private Matcher<FieldMapping> isWildcardSpecified(boolean isMatched) {
85116
return MatcherUtils.featureValueOf("is field match wildcard specified in query",
86117
is(isMatched),
@@ -93,6 +124,12 @@ private Matcher<FieldMapping> isPropertyField(boolean isProperty) {
93124
FieldMapping::isPropertyField);
94125
}
95126

127+
private Matcher<FieldMapping> hasType(String expected) {
128+
return MatcherUtils.featureValueOf("type",
129+
is(expected),
130+
FieldMapping::type);
131+
}
132+
96133
private Map<String, Field> fieldsSpecifiedInQuery(String...fieldNames) {
97134
return Arrays.stream(fieldNames).
98135
collect(Collectors.toMap(name -> name,
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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.esintgtest;
18+
19+
import static com.amazon.opendistroforelasticsearch.sql.esintgtest.TestsConstants.TEST_INDEX_DEEP_NESTED;
20+
import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.rows;
21+
import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.schema;
22+
import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.verifyDataRows;
23+
import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.verifySchema;
24+
25+
import com.amazon.opendistroforelasticsearch.sql.utils.StringUtils;
26+
import org.json.JSONArray;
27+
import org.json.JSONObject;
28+
import org.junit.Test;
29+
30+
/**
31+
* Integration test for Elasticsearch object field (and nested field).
32+
* This class is focused on simple SELECT-FROM query to ensure right column
33+
* number and value is returned.
34+
*/
35+
public class ObjectFieldSelectIT extends SQLIntegTestCase {
36+
37+
@Override
38+
protected void init() throws Exception {
39+
loadIndex(Index.DEEP_NESTED);
40+
}
41+
42+
@Test
43+
public void testSelectObjectFieldItself() {
44+
JSONObject response = new JSONObject(query("SELECT city FROM %s"));
45+
46+
verifySchema(response, schema("city", null, "object"));
47+
48+
// Expect object field itself is returned in a single cell
49+
verifyDataRows(response,
50+
rows(new JSONObject(
51+
"{\n"
52+
+ " \"name\": \"Seattle\",\n"
53+
+ " \"location\": {\"latitude\": 10.5}\n"
54+
+ "}")
55+
)
56+
);
57+
}
58+
59+
@Test
60+
public void testSelectObjectInnerFields() {
61+
JSONObject response = new JSONObject(query(
62+
"SELECT city.location, city.location.latitude FROM %s"));
63+
64+
verifySchema(response,
65+
schema("city.location", null, "object"),
66+
schema("city.location.latitude", null, "double")
67+
);
68+
69+
// Expect inner regular or object field returned in its single cell
70+
verifyDataRows(response,
71+
rows(
72+
new JSONObject("{\"latitude\": 10.5}"),
73+
10.5
74+
)
75+
);
76+
}
77+
78+
@Test
79+
public void testSelectNestedFieldItself() {
80+
JSONObject response = new JSONObject(query("SELECT projects FROM %s"));
81+
82+
// Nested field is absent in ES Get Field Mapping response either hence "object" used
83+
verifySchema(response, schema("projects", null, "object"));
84+
85+
// Expect nested field itself is returned in a single cell
86+
verifyDataRows(response,
87+
rows(new JSONArray(
88+
"[\n"
89+
+ " {\"name\": \"AWS Redshift Spectrum querying\"},\n"
90+
+ " {\"name\": \"AWS Redshift security\"},\n"
91+
+ " {\"name\": \"AWS Aurora security\"}\n"
92+
+ "]")
93+
)
94+
);
95+
}
96+
97+
@Test
98+
public void testSelectObjectFieldOfArrayValuesItself() {
99+
JSONObject response = new JSONObject(query("SELECT accounts FROM %s"));
100+
101+
// Expect the entire list of values is returned just like a nested field
102+
verifyDataRows(response,
103+
rows(new JSONArray(
104+
"[\n"
105+
+ " {\"id\": 1},\n"
106+
+ " {\"id\": 2}\n"
107+
+ "]")
108+
)
109+
);
110+
}
111+
112+
@Test
113+
public void testSelectObjectFieldOfArrayValuesInnerFields() {
114+
JSONObject response = new JSONObject(query("SELECT accounts.id FROM %s"));
115+
116+
// We don't support flatten object field of list value so expect null returned
117+
verifyDataRows(response, rows(JSONObject.NULL));
118+
}
119+
120+
private String query(String sql) {
121+
return executeQuery(
122+
StringUtils.format(sql, TEST_INDEX_DEEP_NESTED),
123+
"jdbc"
124+
);
125+
}
126+
127+
}

0 commit comments

Comments
 (0)