Skip to content

Commit f097818

Browse files
authored
ESQL: Report original_types (elastic#124913)
Adds the `original_types` to the description of ESQL's `unsupported` fields. This looks like: ``` { "name" : "a", "type" : "unsupported", "original_types" : [ "long", "text" ] } ``` for union types. And like: ``` { "name" : "a", "type" : "unsupported", "original_types" : [ "date_range" ] } ``` for truly unsupported types. This information is useful for the UI. For union types it can suggest that users append a cast.
1 parent 6be0bf5 commit f097818

File tree

23 files changed

+377
-207
lines changed

23 files changed

+377
-207
lines changed

docs/changelog/124913.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 124913
2+
summary: Report `original_types`
3+
area: ES|QL
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/TransportVersions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ static TransportVersion def(int id) {
203203
public static final TransportVersion ESQL_AGGREGATE_METRIC_DOUBLE_LITERAL = def(9_035_0_00);
204204
public static final TransportVersion INDEX_METADATA_INCLUDES_RECENT_WRITE_LOAD = def(9_036_0_00);
205205
public static final TransportVersion RERANK_COMMON_OPTIONS_ADDED = def(9_037_0_00);
206+
public static final TransportVersion ESQL_REPORT_ORIGINAL_TYPES = def(9_038_00_0);
206207

207208
/*
208209
* STOP! READ THIS FIRST! No, really,

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/expression/Attribute.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,14 @@ public String nodeString() {
137137
}
138138

139139
protected abstract String label();
140+
141+
/**
142+
* If this field is unsupported this contains the underlying ES types. If there
143+
* is a type conflict this will have many elements, some or all of which may
144+
* be actually supported types.
145+
*/
146+
@Nullable
147+
public List<String> originalTypes() {
148+
return null;
149+
}
140150
}

x-pack/plugin/esql-core/src/main/java/org/elasticsearch/xpack/esql/core/type/UnsupportedEsField.java

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
*/
77
package org.elasticsearch.xpack.esql.core.type;
88

9+
import org.elasticsearch.TransportVersions;
910
import org.elasticsearch.common.io.stream.StreamInput;
1011
import org.elasticsearch.common.io.stream.StreamOutput;
12+
import org.elasticsearch.xpack.esql.core.util.PlanStreamInput;
13+
import org.elasticsearch.xpack.esql.core.util.PlanStreamOutput;
1114

1215
import java.io.IOException;
16+
import java.util.List;
1317
import java.util.Map;
1418
import java.util.Objects;
1519
import java.util.TreeMap;
@@ -23,32 +27,39 @@
2327
*/
2428
public class UnsupportedEsField extends EsField {
2529

26-
private final String originalType;
30+
private final List<String> originalTypes;
2731
private final String inherited; // for fields belonging to parents (or grandparents) that have an unsupported type
2832

29-
public UnsupportedEsField(String name, String originalType) {
30-
this(name, originalType, null, new TreeMap<>());
33+
public UnsupportedEsField(String name, List<String> originalTypes) {
34+
this(name, originalTypes, null, new TreeMap<>());
3135
}
3236

33-
public UnsupportedEsField(String name, String originalType, String inherited, Map<String, EsField> properties) {
37+
public UnsupportedEsField(String name, List<String> originalTypes, String inherited, Map<String, EsField> properties) {
3438
super(name, DataType.UNSUPPORTED, properties, false);
35-
this.originalType = originalType;
39+
this.originalTypes = originalTypes;
3640
this.inherited = inherited;
3741
}
3842

3943
public UnsupportedEsField(StreamInput in) throws IOException {
40-
this(
41-
readCachedStringWithVersionCheck(in),
42-
readCachedStringWithVersionCheck(in),
43-
in.readOptionalString(),
44-
in.readImmutableMap(EsField::readFrom)
45-
);
44+
this(readCachedStringWithVersionCheck(in), readOriginalTypes(in), in.readOptionalString(), in.readImmutableMap(EsField::readFrom));
45+
}
46+
47+
private static List<String> readOriginalTypes(StreamInput in) throws IOException {
48+
if (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_REPORT_ORIGINAL_TYPES)) {
49+
return in.readCollectionAsList(i -> ((PlanStreamInput) i).readCachedString());
50+
} else {
51+
return List.of(readCachedStringWithVersionCheck(in).split(","));
52+
}
4653
}
4754

4855
@Override
4956
public void writeContent(StreamOutput out) throws IOException {
5057
writeCachedStringWithVersionCheck(out, getName());
51-
writeCachedStringWithVersionCheck(out, getOriginalType());
58+
if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_REPORT_ORIGINAL_TYPES)) {
59+
out.writeCollection(getOriginalTypes(), (o, s) -> ((PlanStreamOutput) o).writeCachedString(s));
60+
} else {
61+
writeCachedStringWithVersionCheck(out, String.join(",", getOriginalTypes()));
62+
}
5263
out.writeOptionalString(getInherited());
5364
out.writeMap(getProperties(), (o, x) -> x.writeTo(out));
5465
}
@@ -57,8 +68,8 @@ public String getWriteableName() {
5768
return "UnsupportedEsField";
5869
}
5970

60-
public String getOriginalType() {
61-
return originalType;
71+
public List<String> getOriginalTypes() {
72+
return originalTypes;
6273
}
6374

6475
public String getInherited() {
@@ -81,11 +92,11 @@ public boolean equals(Object o) {
8192
return false;
8293
}
8394
UnsupportedEsField that = (UnsupportedEsField) o;
84-
return Objects.equals(originalType, that.originalType) && Objects.equals(inherited, that.inherited);
95+
return Objects.equals(originalTypes, that.originalTypes) && Objects.equals(inherited, that.inherited);
8596
}
8697

8798
@Override
8899
public int hashCode() {
89-
return Objects.hash(super.hashCode(), originalType, inherited);
100+
return Objects.hash(super.hashCode(), originalTypes, inherited);
90101
}
91102
}

x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -327,12 +327,13 @@ public void testAliasToInt() throws IOException {
327327
}
328328

329329
public void testFlattenedUnsupported() throws IOException {
330+
assumeOriginalTypesReported();
330331
new Test("flattened").createIndex("test", "flattened");
331332
index("test", """
332333
{"flattened": {"a": "foo"}}""");
333334
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
334335

335-
assertResultMap(result, List.of(columnInfo("flattened", "unsupported")), List.of(matchesList().item(null)));
336+
assertResultMap(result, List.of(unsupportedColumnInfo("flattened", "flattened")), List.of(matchesList().item(null)));
336337
}
337338

338339
public void testEmptyMapping() throws IOException {
@@ -689,6 +690,7 @@ public void testByteFieldWithIntSubfieldTooBig() throws IOException {
689690
* </pre>.
690691
*/
691692
public void testIncompatibleTypes() throws IOException {
693+
assumeOriginalTypesReported();
692694
keywordTest().createIndex("test1", "f");
693695
index("test1", """
694696
{"f": "f1"}""");
@@ -697,7 +699,11 @@ public void testIncompatibleTypes() throws IOException {
697699
{"f": 1}""");
698700

699701
Map<String, Object> result = runEsql("FROM test*");
700-
assertResultMap(result, List.of(columnInfo("f", "unsupported")), List.of(matchesList().item(null), matchesList().item(null)));
702+
assertResultMap(
703+
result,
704+
List.of(unsupportedColumnInfo("f", "keyword", "long")),
705+
List.of(matchesList().item(null), matchesList().item(null))
706+
);
701707
ResponseException e = expectThrows(ResponseException.class, () -> runEsql("FROM test* | SORT f | LIMIT 3"));
702708
String err = EntityUtils.toString(e.getResponse().getEntity());
703709
assertThat(
@@ -758,10 +764,7 @@ public void testDistinctInEachIndex() throws IOException {
758764
* </pre>.
759765
*/
760766
public void testMergeKeywordAndObject() throws IOException {
761-
assumeTrue(
762-
"order of fields in error message inconsistent before 8.14",
763-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
764-
);
767+
assumeOriginalTypesReported();
765768
keywordTest().createIndex("test1", "file");
766769
index("test1", """
767770
{"file": "f1"}""");
@@ -797,7 +800,7 @@ public void testMergeKeywordAndObject() throws IOException {
797800
Map<String, Object> result = runEsql("FROM test* | SORT file.raw | LIMIT 2");
798801
assertResultMap(
799802
result,
800-
List.of(columnInfo("file", "unsupported"), columnInfo("file.raw", "keyword")),
803+
List.of(unsupportedColumnInfo("file", "keyword", "object"), columnInfo("file.raw", "keyword")),
801804
List.of(matchesList().item(null).item("o2"), matchesList().item(null).item(null))
802805
);
803806
}
@@ -817,6 +820,7 @@ public void testMergeKeywordAndObject() throws IOException {
817820
* </pre>.
818821
*/
819822
public void testPropagateUnsupportedToSubFields() throws IOException {
823+
assumeOriginalTypesReported();
820824
createIndex("test", index -> {
821825
index.startObject("properties");
822826
index.startObject("f");
@@ -842,7 +846,7 @@ public void testPropagateUnsupportedToSubFields() throws IOException {
842846
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
843847
assertResultMap(
844848
result,
845-
List.of(columnInfo("f", "unsupported"), columnInfo("f.raw", "unsupported")),
849+
List.of(unsupportedColumnInfo("f", "ip_range"), unsupportedColumnInfo("f.raw", "ip_range")),
846850
List.of(matchesList().item(null).item(null))
847851
);
848852
}
@@ -867,10 +871,7 @@ public void testPropagateUnsupportedToSubFields() throws IOException {
867871
* </pre>.
868872
*/
869873
public void testMergeUnsupportedAndObject() throws IOException {
870-
assumeTrue(
871-
"order of fields in error message inconsistent before 8.14",
872-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
873-
);
874+
assumeOriginalTypesReported();
874875
createIndex("test1", index -> {
875876
index.startObject("properties");
876877
index.startObject("f").field("type", "ip_range").endObject();
@@ -905,7 +906,7 @@ public void testMergeUnsupportedAndObject() throws IOException {
905906
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
906907
assertResultMap(
907908
result,
908-
List.of(columnInfo("f", "unsupported"), columnInfo("f.raw", "unsupported")),
909+
List.of(unsupportedColumnInfo("f", "ip_range"), unsupportedColumnInfo("f.raw", "ip_range")),
909910
List.of(matchesList().item(null).item(null), matchesList().item(null).item(null))
910911
);
911912
}
@@ -958,10 +959,7 @@ public void testIntegerDocValuesConflict() throws IOException {
958959
* In an ideal world we'd promote the {@code integer} to an {@code long} and just go.
959960
*/
960961
public void testLongIntegerConflict() throws IOException {
961-
assumeTrue(
962-
"order of fields in error message inconsistent before 8.14",
963-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
964-
);
962+
assumeOriginalTypesReported();
965963
longTest().sourceMode(SourceMode.DEFAULT).createIndex("test1", "emp_no");
966964
index("test1", """
967965
{"emp_no": 1}""");
@@ -980,7 +978,11 @@ public void testLongIntegerConflict() throws IOException {
980978
);
981979

982980
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
983-
assertResultMap(result, List.of(columnInfo("emp_no", "unsupported")), List.of(matchesList().item(null), matchesList().item(null)));
981+
assertResultMap(
982+
result,
983+
List.of(unsupportedColumnInfo("emp_no", "integer", "long")),
984+
List.of(matchesList().item(null), matchesList().item(null))
985+
);
984986
}
985987

986988
/**
@@ -1000,10 +1002,7 @@ public void testLongIntegerConflict() throws IOException {
10001002
* In an ideal world we'd promote the {@code short} to an {@code integer} and just go.
10011003
*/
10021004
public void testIntegerShortConflict() throws IOException {
1003-
assumeTrue(
1004-
"order of fields in error message inconsistent before 8.14",
1005-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
1006-
);
1005+
assumeOriginalTypesReported();
10071006
intTest().sourceMode(SourceMode.DEFAULT).createIndex("test1", "emp_no");
10081007
index("test1", """
10091008
{"emp_no": 1}""");
@@ -1022,7 +1021,11 @@ public void testIntegerShortConflict() throws IOException {
10221021
);
10231022

10241023
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
1025-
assertResultMap(result, List.of(columnInfo("emp_no", "unsupported")), List.of(matchesList().item(null), matchesList().item(null)));
1024+
assertResultMap(
1025+
result,
1026+
List.of(unsupportedColumnInfo("emp_no", "integer", "short")),
1027+
List.of(matchesList().item(null), matchesList().item(null))
1028+
);
10261029
}
10271030

10281031
/**
@@ -1048,10 +1051,7 @@ public void testIntegerShortConflict() throws IOException {
10481051
* </pre>.
10491052
*/
10501053
public void testTypeConflictInObject() throws IOException {
1051-
assumeTrue(
1052-
"order of fields in error message inconsistent before 8.14",
1053-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
1054-
);
1054+
assumeOriginalTypesReported();
10551055
createIndex("test1", empNoInObject("integer"));
10561056
index("test1", """
10571057
{"foo": {"emp_no": 1}}""");
@@ -1060,7 +1060,10 @@ public void testTypeConflictInObject() throws IOException {
10601060
{"foo": {"emp_no": "cat"}}""");
10611061

10621062
Map<String, Object> result = runEsql("FROM test* | LIMIT 3");
1063-
assertMap(result, getResultMatcher(result).entry("columns", List.of(columnInfo("foo.emp_no", "unsupported"))).extraOk());
1063+
assertMap(
1064+
result,
1065+
getResultMatcher(result).entry("columns", List.of(unsupportedColumnInfo("foo.emp_no", "integer", "keyword"))).extraOk()
1066+
);
10641067

10651068
ResponseException e = expectThrows(ResponseException.class, () -> runEsql("FROM test* | SORT foo.emp_no | LIMIT 3"));
10661069
String err = EntityUtils.toString(e.getResponse().getEntity());
@@ -1370,6 +1373,12 @@ private void assumeIndexResolverNestedFieldsNameClashFixed() throws IOException
13701373
);
13711374
}
13721375

1376+
private void assumeOriginalTypesReported() throws IOException {
1377+
var capsName = EsqlCapabilities.Cap.REPORT_ORIGINAL_TYPES.name().toLowerCase(Locale.ROOT);
1378+
boolean requiredClusterCapability = clusterHasCapability("POST", "/_query", List.of(), List.of(capsName)).orElse(false);
1379+
assumeTrue("This test makes sense for versions that report original types", requiredClusterCapability);
1380+
}
1381+
13731382
private CheckedConsumer<XContentBuilder, IOException> empNoInObject(String empNoType) {
13741383
return index -> {
13751384
index.startObject("properties");
@@ -1705,6 +1714,10 @@ private static Map<String, Object> columnInfo(String name, String type) {
17051714
return Map.of("name", name, "type", type);
17061715
}
17071716

1717+
private static Map<String, Object> unsupportedColumnInfo(String name, String... originalTypes) {
1718+
return Map.of("name", name, "type", "unsupported", "original_types", List.of(originalTypes));
1719+
}
1720+
17081721
private static void index(String name, String... docs) throws IOException {
17091722
Request request = new Request("POST", "/" + name + "/_bulk");
17101723
request.addParameter("refresh", "true");

x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/LoadMapping.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.io.InputStream;
2525
import java.util.Collections;
2626
import java.util.LinkedHashMap;
27+
import java.util.List;
2728
import java.util.Map;
2829

2930
import static java.util.Collections.emptyMap;
@@ -110,7 +111,7 @@ private static void walkMapping(String name, Object value, Map<String, EsField>
110111
field = DateEsField.dateEsField(name, properties, docValues);
111112
} else if (esDataType == UNSUPPORTED) {
112113
String type = content.get("type").toString();
113-
field = new UnsupportedEsField(name, type, null, properties);
114+
field = new UnsupportedEsField(name, List.of(type), null, properties);
114115
propagateUnsupportedType(name, type, properties);
115116
} else {
116117
field = new EsField(name, esDataType, properties, docValues);
@@ -165,9 +166,9 @@ public static void propagateUnsupportedType(String inherited, String originalTyp
165166
UnsupportedEsField u;
166167
if (field instanceof UnsupportedEsField) {
167168
u = (UnsupportedEsField) field;
168-
u = new UnsupportedEsField(u.getName(), originalType, inherited, u.getProperties());
169+
u = new UnsupportedEsField(u.getName(), List.of(originalType), inherited, u.getProperties());
169170
} else {
170-
u = new UnsupportedEsField(field.getName(), originalType, inherited, field.getProperties());
171+
u = new UnsupportedEsField(field.getName(), List.of(originalType), inherited, field.getProperties());
171172
}
172173
entry.setValue(u);
173174
propagateUnsupportedType(inherited, originalType, u.getProperties());

x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AsyncEsqlQueryActionIT.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public void testBasicAsyncExecution() throws Exception {
9494
try (var finalResponse = future.get()) {
9595
assertThat(finalResponse, notNullValue());
9696
assertThat(finalResponse.isRunning(), is(false));
97-
assertThat(finalResponse.columns(), equalTo(List.of(new ColumnInfoImpl("sum(pause_me)", "long"))));
97+
assertThat(finalResponse.columns(), equalTo(List.of(new ColumnInfoImpl("sum(pause_me)", "long", null))));
9898
assertThat(getValuesList(finalResponse).size(), equalTo(1));
9999
}
100100

@@ -103,7 +103,7 @@ public void testBasicAsyncExecution() throws Exception {
103103
try (var finalResponse = again.get()) {
104104
assertThat(finalResponse, notNullValue());
105105
assertThat(finalResponse.isRunning(), is(false));
106-
assertThat(finalResponse.columns(), equalTo(List.of(new ColumnInfoImpl("sum(pause_me)", "long"))));
106+
assertThat(finalResponse.columns(), equalTo(List.of(new ColumnInfoImpl("sum(pause_me)", "long", null))));
107107
assertThat(getValuesList(finalResponse).size(), equalTo(1));
108108
}
109109

@@ -231,7 +231,7 @@ private void testFinishingBeforeTimeout(boolean keepOnCompletion) {
231231

232232
try (var response = request.execute().actionGet(60, TimeUnit.SECONDS)) {
233233
assertThat(response.isRunning(), is(false));
234-
assertThat(response.columns(), equalTo(List.of(new ColumnInfoImpl("sum(pause_me)", "long"))));
234+
assertThat(response.columns(), equalTo(List.of(new ColumnInfoImpl("sum(pause_me)", "long", null))));
235235
assertThat(getValuesList(response).size(), equalTo(1));
236236

237237
if (keepOnCompletion) {
@@ -244,7 +244,7 @@ private void testFinishingBeforeTimeout(boolean keepOnCompletion) {
244244
try (var resp = future.actionGet(60, TimeUnit.SECONDS)) {
245245
assertThat(resp.asyncExecutionId().get(), equalTo(id));
246246
assertThat(resp.isRunning(), is(false));
247-
assertThat(resp.columns(), equalTo(List.of(new ColumnInfoImpl("sum(pause_me)", "long"))));
247+
assertThat(resp.columns(), equalTo(List.of(new ColumnInfoImpl("sum(pause_me)", "long", null))));
248248
assertThat(getValuesList(resp).size(), equalTo(1));
249249
}
250250
} else {

0 commit comments

Comments
 (0)