Skip to content

Commit 496a842

Browse files
authored
SQL: Add xDBC and CLI support. QA CSV specs (#68966)
This adds support for the xDBC and CLI clients. Based on those, the CsvJdbc-based QA tests are also integrated.
1 parent 115f880 commit 496a842

File tree

42 files changed

+1340
-173
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1340
-173
lines changed

x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/execution/search/extractor/AbstractFieldHitExtractor.java

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717

1818
import java.io.IOException;
1919
import java.time.ZoneId;
20+
import java.util.ArrayList;
2021
import java.util.List;
2122
import java.util.Map;
2223
import java.util.Objects;
2324

25+
import static java.util.Collections.emptyList;
2426
import static java.util.Collections.singletonList;
2527

2628
/**
@@ -47,7 +49,7 @@ public Object handle(Object object, String fieldName) {
4749
EXTRACT_ARRAY {
4850
@Override
4951
public Object handle(Object object, String _ignored) {
50-
return object instanceof List ? object : singletonList(object);
52+
return object == null ? emptyList() : (object instanceof List ? object : singletonList(object));
5153
}
5254
};
5355

@@ -127,18 +129,10 @@ public void writeTo(StreamOutput out) throws IOException {
127129

128130
@Override
129131
public Object extract(SearchHit hit) {
130-
Object value = null;
131-
DocumentField field = null;
132-
if (hitName != null) {
133-
// a nested field value is grouped under the nested parent name (ie dep.dep_name lives under "dep":[{dep_name:value}])
134-
field = hit.field(hitName);
135-
} else {
136-
field = hit.field(fieldName);
137-
}
138-
if (field != null) {
139-
value = unwrapFieldsMultiValue(field.getValues());
140-
}
141-
return value;
132+
// hitName: a nested field value is grouped under the nested parent name (ie dep.dep_name lives under "dep":[{dep_name:value}])
133+
String lookupName = hitName != null ? hitName : fieldName;
134+
DocumentField field = hit.field(lookupName);
135+
return multiValueHandling.handle(field != null ? unwrapFieldsMultiValue(field.getValues()) : null, lookupName);
142136
}
143137

144138
protected Object unwrapFieldsMultiValue(Object values) {
@@ -155,11 +149,16 @@ protected Object unwrapFieldsMultiValue(Object values) {
155149
return null;
156150
} else {
157151
if (isPrimitive(list) == false) {
158-
if (list.size() == 1 || multiValueHandling == MultiValueHandling.EXTRACT_ONE) {
159-
return unwrapFieldsMultiValue(list.get(0));
160-
} else {
161-
throw new QlIllegalArgumentException("Arrays (returned by [{}]) are not supported", fieldName);
152+
List<Object> unwrappedList = new ArrayList<>();
153+
for (Object o : list) {
154+
Object unwrapped = unwrapFieldsMultiValue(o);
155+
if (unwrapped instanceof List) {
156+
unwrappedList.addAll((List<?>) unwrapped);
157+
} else {
158+
unwrappedList.add(unwrapped);
159+
}
162160
}
161+
return unwrappedList;
163162
}
164163
}
165164
}

x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/EsType.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,24 @@ public enum EsType implements SQLType {
4848
INTERVAL_MINUTE_TO_SECOND(ExtraTypes.INTERVAL_MINUTE_SECOND),
4949
GEO_POINT( ExtraTypes.GEOMETRY),
5050
GEO_SHAPE( ExtraTypes.GEOMETRY),
51-
SHAPE( ExtraTypes.GEOMETRY);
51+
SHAPE( ExtraTypes.GEOMETRY),
52+
BOOLEAN_ARRAY( Types.ARRAY),
53+
BYTE_ARRAY( Types.ARRAY),
54+
SHORT_ARRAY( Types.ARRAY),
55+
INTEGER_ARRAY( Types.ARRAY),
56+
LONG_ARRAY( Types.ARRAY),
57+
DOUBLE_ARRAY( Types.ARRAY),
58+
FLOAT_ARRAY( Types.ARRAY),
59+
HALF_FLOAT_ARRAY( Types.ARRAY),
60+
SCALED_FLOAT_ARRAY( Types.ARRAY),
61+
KEYWORD_ARRAY( Types.ARRAY),
62+
TEXT_ARRAY( Types.ARRAY),
63+
DATETIME_ARRAY( Types.ARRAY),
64+
IP_ARRAY( Types.ARRAY),
65+
BINARY_ARRAY( Types.ARRAY),
66+
GEO_SHAPE_ARRAY( Types.ARRAY),
67+
GEO_POINT_ARRAY( Types.ARRAY),
68+
SHAPE_ARRAY( Types.ARRAY);
5269

5370
private final Integer type;
5471

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.sql.jdbc;
9+
10+
import java.sql.Array;
11+
import java.sql.ResultSet;
12+
import java.sql.SQLException;
13+
import java.sql.SQLFeatureNotSupportedException;
14+
import java.util.Arrays;
15+
import java.util.List;
16+
import java.util.Map;
17+
18+
import static org.elasticsearch.xpack.sql.jdbc.TypeUtils.baseType;
19+
20+
public class JdbcArray implements Array {
21+
22+
private final EsType type;
23+
private final List<?> values;
24+
25+
public JdbcArray(EsType type, List<?> values) {
26+
this.type = type;
27+
this.values = values;
28+
}
29+
@Override
30+
public String getBaseTypeName() throws SQLException {
31+
return baseType(type).getName();
32+
}
33+
34+
@Override
35+
public int getBaseType() throws SQLException {
36+
return baseType(type).getVendorTypeNumber();
37+
}
38+
39+
@Override
40+
public Object getArray() throws SQLException {
41+
return values.toArray();
42+
}
43+
44+
@Override
45+
public Object getArray(Map<String, Class<?>> map) throws SQLException {
46+
if (map == null || map.isEmpty()) {
47+
return getArray();
48+
}
49+
throw new SQLFeatureNotSupportedException("getArray with non-empty Map not supported");
50+
}
51+
52+
@Override
53+
public Object getArray(long index, int count) throws SQLException {
54+
if (index < 1 || index > Integer.MAX_VALUE) {
55+
throw new SQLException("Index value [" + index + "] out of range [1, " + Integer.MAX_VALUE + "]");
56+
}
57+
if (count < 0) {
58+
throw new SQLException("Illegal negative count [" + count + "]");
59+
}
60+
int index0 = (int) index - 1; // 0-based index
61+
int available = index0 < values.size() ? values.size() - index0 : 0;
62+
return available > 0 ?
63+
Arrays.copyOfRange(values.toArray(), index0, index0 + Math.min(count, available)):
64+
new Object[0];
65+
}
66+
67+
@Override
68+
public Object getArray(long index, int count, Map<String, Class<?>> map) throws SQLException {
69+
if (map == null || map.isEmpty()) {
70+
return getArray(index, count);
71+
}
72+
throw new SQLFeatureNotSupportedException("getArray with non-empty Map not supported");
73+
}
74+
75+
@Override
76+
public ResultSet getResultSet() throws SQLException {
77+
throw new SQLFeatureNotSupportedException("Array as ResultSet not supported");
78+
}
79+
80+
@Override
81+
public ResultSet getResultSet(Map<String, Class<?>> map) throws SQLException {
82+
throw new SQLFeatureNotSupportedException("Array as ResultSet not supported");
83+
}
84+
85+
@Override
86+
public ResultSet getResultSet(long index, int count) throws SQLException {
87+
throw new SQLFeatureNotSupportedException("Array as ResultSet not supported");
88+
}
89+
90+
@Override
91+
public ResultSet getResultSet(long index, int count, Map<String, Class<?>> map) throws SQLException {
92+
throw new SQLFeatureNotSupportedException("Array as ResultSet not supported");
93+
}
94+
95+
@Override
96+
public void free() throws SQLException {
97+
// nop
98+
}
99+
}

x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/JdbcResultSet.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.timeAsMillisSinceEpoch;
4545
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.timeAsTime;
4646
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.timeAsTimestamp;
47+
import static org.elasticsearch.xpack.sql.jdbc.TypeUtils.isArray;
4748

4849
class JdbcResultSet implements ResultSet, JdbcWrapper {
4950

@@ -934,7 +935,11 @@ public Clob getClob(int columnIndex) throws SQLException {
934935

935936
@Override
936937
public Array getArray(int columnIndex) throws SQLException {
937-
throw new SQLFeatureNotSupportedException("Array not supported");
938+
EsType type = columnType(columnIndex);
939+
if (isArray(type) == false) {
940+
throw new SQLException("Cannot get column [" + columnIndex + "] of type [" + type.getName() + "] as array");
941+
}
942+
return new JdbcArray(type, (List<?>) getObject(columnIndex));
938943
}
939944

940945
@Override
@@ -954,7 +959,7 @@ public Clob getClob(String columnLabel) throws SQLException {
954959

955960
@Override
956961
public Array getArray(String columnLabel) throws SQLException {
957-
throw new SQLFeatureNotSupportedException("Array not supported");
962+
return getArray(column(columnLabel));
958963
}
959964

960965
@Override

x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/TypeConverter.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
import java.time.Period;
2828
import java.time.ZoneOffset;
2929
import java.time.ZonedDateTime;
30+
import java.util.ArrayList;
3031
import java.util.Calendar;
3132
import java.util.GregorianCalendar;
33+
import java.util.List;
3234
import java.util.Locale;
3335
import java.util.function.Function;
3436

@@ -46,6 +48,7 @@
4648
import static org.elasticsearch.xpack.sql.jdbc.EsType.TIME;
4749
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.asDateTimeField;
4850
import static org.elasticsearch.xpack.sql.jdbc.JdbcDateUtils.timeAsTime;
51+
import static org.elasticsearch.xpack.sql.jdbc.TypeUtils.baseType;
4952

5053
/**
5154
* Conversion utilities for conversion of JDBC types to Java type and back
@@ -263,6 +266,29 @@ static Object convert(Object v, EsType columnType, String typeString) throws SQL
263266
}
264267
case IP:
265268
return v.toString();
269+
case BOOLEAN_ARRAY:
270+
case BYTE_ARRAY:
271+
case SHORT_ARRAY:
272+
case INTEGER_ARRAY:
273+
case LONG_ARRAY:
274+
case DOUBLE_ARRAY:
275+
case FLOAT_ARRAY:
276+
case HALF_FLOAT_ARRAY:
277+
case SCALED_FLOAT_ARRAY:
278+
case KEYWORD_ARRAY:
279+
case TEXT_ARRAY:
280+
case DATETIME_ARRAY:
281+
case IP_ARRAY:
282+
case BINARY_ARRAY:
283+
case GEO_SHAPE_ARRAY:
284+
case GEO_POINT_ARRAY:
285+
case SHAPE_ARRAY:
286+
List<Object> values = new ArrayList<>();
287+
for (Object o : (List<?>) v) {
288+
// null value expects a NULL type in the converter
289+
values.add(o == null ? null : convert(o, baseType(columnType), typeString.substring(0, "_ARRAY".length())));
290+
}
291+
return values;
266292
default:
267293
throw new SQLException("Unexpected column type [" + typeString + "]");
268294

x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/TypeUtils.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.sql.SQLFeatureNotSupportedException;
1313
import java.sql.SQLType;
1414
import java.sql.Timestamp;
15+
import java.sql.Types;
1516
import java.time.Duration;
1617
import java.time.LocalDateTime;
1718
import java.time.Period;
@@ -176,4 +177,22 @@ static EsType of(Class<? extends Object> clazz) throws SQLException {
176177
}
177178
return dataType;
178179
}
180+
181+
static EsType baseType(EsType type) throws SQLException {
182+
String typeName = type.getName();
183+
return isArray(type)
184+
? of(typeName.substring(0, typeName.length() - "_ARRAY".length()).toLowerCase(Locale.ROOT))
185+
: type;
186+
}
187+
188+
static EsType arrayOf(EsType type) throws SQLException {
189+
if (isArray(type)) {
190+
throw new SQLException("multidimentional array types not supported");
191+
}
192+
return ENUM_NAME_TO_TYPE.get(type.name().toLowerCase(Locale.ROOT) + "_array");
193+
}
194+
195+
static boolean isArray(EsType type) {
196+
return type.getVendorTypeNumber() == Types.ARRAY;
197+
}
179198
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.sql.jdbc;
9+
10+
import org.elasticsearch.test.ESTestCase;
11+
12+
import java.sql.Array;
13+
import java.sql.SQLException;
14+
import java.util.Arrays;
15+
import java.util.List;
16+
import java.util.stream.Collectors;
17+
import java.util.stream.IntStream;
18+
19+
import static java.util.Arrays.asList;
20+
import static java.util.Collections.emptyList;
21+
import static org.elasticsearch.xpack.sql.jdbc.TypeUtils.baseType;
22+
23+
public class JdbcArrayTests extends ESTestCase {
24+
25+
static final List<EsType> ARRAY_TYPES = Arrays.stream(EsType.values()).filter(TypeUtils::isArray).collect(Collectors.toList());
26+
27+
public void testMetaData() throws Exception {
28+
for (EsType arrayType : ARRAY_TYPES) {
29+
Array array = new JdbcArray(arrayType, emptyList());
30+
31+
assertEquals(baseType(arrayType).getVendorTypeNumber().intValue(), array.getBaseType());
32+
assertEquals(baseType(arrayType).getName(), array.getBaseTypeName());
33+
}
34+
}
35+
36+
public void testGetArray() throws SQLException {
37+
List<Long> expected = randomList(1, 10, ESTestCase::randomLong);
38+
Array array = new JdbcArray(EsType.LONG_ARRAY, expected);
39+
40+
List<?> actual = asList((Object[]) array.getArray());
41+
assertEquals(expected, actual);
42+
}
43+
44+
public void testArraySlicing() throws SQLException {
45+
List<Integer> values = IntStream.rangeClosed(0, 9).boxed().collect(Collectors.toList());
46+
Array array = new JdbcArray(EsType.INTEGER_ARRAY, values);
47+
48+
Object[] empty = (Object[]) array.getArray(11, 2);
49+
assertEquals(0, empty.length);
50+
51+
Object[] edgeSingleton = (Object[]) array.getArray(10, 2);
52+
assertEquals(9, edgeSingleton[0]);
53+
54+
Object[] midSingleton = (Object[]) array.getArray(5, 1);
55+
assertEquals(4, midSingleton[0]);
56+
57+
Object[] contained = (Object[]) array.getArray(4, 3);
58+
assertEquals(asList(3, 4, 5), asList(contained));
59+
60+
Object[] overlapping = (Object[]) array.getArray(9, 3);
61+
assertEquals(asList(8, 9), asList(overlapping));
62+
63+
SQLException sqle = expectThrows(SQLException.class, () -> array.getArray(0, 9));
64+
assertEquals("Index value [0] out of range [1, 2147483647]", sqle.getMessage());
65+
66+
sqle = expectThrows(SQLException.class, () -> array.getArray(Integer.MAX_VALUE + 1L, 9));
67+
assertEquals("Index value [2147483648] out of range [1, 2147483647]", sqle.getMessage());
68+
}
69+
}

x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/JdbcPreparedStatementTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.time.LocalDateTime;
2222
import java.time.ZonedDateTime;
2323
import java.util.Calendar;
24+
import java.util.Collections;
2425
import java.util.Date;
2526
import java.util.Locale;
2627
import java.util.Map;
@@ -585,6 +586,13 @@ public void testThrownExceptionsWhenSettingByteArrayValuesToEsTypes() throws SQL
585586
assertEquals("Conversion from type [byte[]] to [INTERVAL_DAY_TO_MINUTE] not supported", sqle.getMessage());
586587
}
587588

589+
public void testThrownExceptionWhenSettingArrayValue() throws SQLException {
590+
JdbcPreparedStatement jps = createJdbcPreparedStatement();
591+
JdbcArray array = new JdbcArray(LONG, Collections.emptyList());
592+
SQLException sqle = expectThrows(SQLFeatureNotSupportedException.class, () -> jps.setArray(1, array));
593+
assertEquals("Objects of type [java.sql.Array] are not supported", sqle.getMessage());
594+
}
595+
588596
private JdbcPreparedStatement createJdbcPreparedStatement() throws SQLException {
589597
return new JdbcPreparedStatement(null, JdbcConfiguration.create("jdbc:es://l:1", null, 0), "?");
590598
}

0 commit comments

Comments
 (0)