Skip to content

Commit fd19e89

Browse files
authored
ESQL: Report original_types (elastic#124913) (elastic#127641)
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 d78dbfb commit fd19e89

File tree

23 files changed

+378
-207
lines changed

23 files changed

+378
-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
@@ -208,6 +208,7 @@ static TransportVersion def(int id) {
208208
public static final TransportVersion BATCHED_QUERY_PHASE_VERSION_BACKPORT_8_X = def(8_841_0_19);
209209
public static final TransportVersion SEARCH_INCREMENTAL_TOP_DOCS_NULL_BACKPORT_8_19 = def(8_841_0_20);
210210
public static final TransportVersion ML_INFERENCE_SAGEMAKER_8_19 = def(8_841_0_21);
211+
public static final TransportVersion ESQL_REPORT_ORIGINAL_TYPES_BACKPORT_8_19 = def(8_841_0_22);
211212

212213
/*
213214
* 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_BACKPORT_8_19)) {
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_BACKPORT_8_19)) {
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
@@ -302,12 +302,13 @@ public void testAliasToInt() throws IOException {
302302
}
303303

304304
public void testFlattenedUnsupported() throws IOException {
305+
assumeOriginalTypesReported();
305306
new Test("flattened").createIndex("test", "flattened");
306307
index("test", """
307308
{"flattened": {"a": "foo"}}""");
308309
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
309310

310-
assertResultMap(result, List.of(columnInfo("flattened", "unsupported")), List.of(matchesList().item(null)));
311+
assertResultMap(result, List.of(unsupportedColumnInfo("flattened", "flattened")), List.of(matchesList().item(null)));
311312
}
312313

313314
public void testEmptyMapping() throws IOException {
@@ -664,6 +665,7 @@ public void testByteFieldWithIntSubfieldTooBig() throws IOException {
664665
* </pre>.
665666
*/
666667
public void testIncompatibleTypes() throws IOException {
668+
assumeOriginalTypesReported();
667669
keywordTest().createIndex("test1", "f");
668670
index("test1", """
669671
{"f": "f1"}""");
@@ -672,7 +674,11 @@ public void testIncompatibleTypes() throws IOException {
672674
{"f": 1}""");
673675

674676
Map<String, Object> result = runEsql("FROM test*");
675-
assertResultMap(result, List.of(columnInfo("f", "unsupported")), List.of(matchesList().item(null), matchesList().item(null)));
677+
assertResultMap(
678+
result,
679+
List.of(unsupportedColumnInfo("f", "keyword", "long")),
680+
List.of(matchesList().item(null), matchesList().item(null))
681+
);
676682
ResponseException e = expectThrows(ResponseException.class, () -> runEsql("FROM test* | SORT f | LIMIT 3"));
677683
String err = EntityUtils.toString(e.getResponse().getEntity());
678684
assertThat(
@@ -733,10 +739,7 @@ public void testDistinctInEachIndex() throws IOException {
733739
* </pre>.
734740
*/
735741
public void testMergeKeywordAndObject() throws IOException {
736-
assumeTrue(
737-
"order of fields in error message inconsistent before 8.14",
738-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
739-
);
742+
assumeOriginalTypesReported();
740743
keywordTest().createIndex("test1", "file");
741744
index("test1", """
742745
{"file": "f1"}""");
@@ -772,7 +775,7 @@ public void testMergeKeywordAndObject() throws IOException {
772775
Map<String, Object> result = runEsql("FROM test* | SORT file.raw | LIMIT 2");
773776
assertResultMap(
774777
result,
775-
List.of(columnInfo("file", "unsupported"), columnInfo("file.raw", "keyword")),
778+
List.of(unsupportedColumnInfo("file", "keyword", "object"), columnInfo("file.raw", "keyword")),
776779
List.of(matchesList().item(null).item("o2"), matchesList().item(null).item(null))
777780
);
778781
}
@@ -792,6 +795,7 @@ public void testMergeKeywordAndObject() throws IOException {
792795
* </pre>.
793796
*/
794797
public void testPropagateUnsupportedToSubFields() throws IOException {
798+
assumeOriginalTypesReported();
795799
createIndex("test", index -> {
796800
index.startObject("properties");
797801
index.startObject("f");
@@ -817,7 +821,7 @@ public void testPropagateUnsupportedToSubFields() throws IOException {
817821
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
818822
assertResultMap(
819823
result,
820-
List.of(columnInfo("f", "unsupported"), columnInfo("f.raw", "unsupported")),
824+
List.of(unsupportedColumnInfo("f", "ip_range"), unsupportedColumnInfo("f.raw", "ip_range")),
821825
List.of(matchesList().item(null).item(null))
822826
);
823827
}
@@ -842,10 +846,7 @@ public void testPropagateUnsupportedToSubFields() throws IOException {
842846
* </pre>.
843847
*/
844848
public void testMergeUnsupportedAndObject() throws IOException {
845-
assumeTrue(
846-
"order of fields in error message inconsistent before 8.14",
847-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
848-
);
849+
assumeOriginalTypesReported();
849850
createIndex("test1", index -> {
850851
index.startObject("properties");
851852
index.startObject("f").field("type", "ip_range").endObject();
@@ -880,7 +881,7 @@ public void testMergeUnsupportedAndObject() throws IOException {
880881
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
881882
assertResultMap(
882883
result,
883-
List.of(columnInfo("f", "unsupported"), columnInfo("f.raw", "unsupported")),
884+
List.of(unsupportedColumnInfo("f", "ip_range"), unsupportedColumnInfo("f.raw", "ip_range")),
884885
List.of(matchesList().item(null).item(null), matchesList().item(null).item(null))
885886
);
886887
}
@@ -933,10 +934,7 @@ public void testIntegerDocValuesConflict() throws IOException {
933934
* In an ideal world we'd promote the {@code integer} to an {@code long} and just go.
934935
*/
935936
public void testLongIntegerConflict() throws IOException {
936-
assumeTrue(
937-
"order of fields in error message inconsistent before 8.14",
938-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
939-
);
937+
assumeOriginalTypesReported();
940938
longTest().sourceMode(SourceMode.DEFAULT).createIndex("test1", "emp_no");
941939
index("test1", """
942940
{"emp_no": 1}""");
@@ -955,7 +953,11 @@ public void testLongIntegerConflict() throws IOException {
955953
);
956954

957955
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
958-
assertResultMap(result, List.of(columnInfo("emp_no", "unsupported")), List.of(matchesList().item(null), matchesList().item(null)));
956+
assertResultMap(
957+
result,
958+
List.of(unsupportedColumnInfo("emp_no", "integer", "long")),
959+
List.of(matchesList().item(null), matchesList().item(null))
960+
);
959961
}
960962

961963
/**
@@ -975,10 +977,7 @@ public void testLongIntegerConflict() throws IOException {
975977
* In an ideal world we'd promote the {@code short} to an {@code integer} and just go.
976978
*/
977979
public void testIntegerShortConflict() throws IOException {
978-
assumeTrue(
979-
"order of fields in error message inconsistent before 8.14",
980-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
981-
);
980+
assumeOriginalTypesReported();
982981
intTest().sourceMode(SourceMode.DEFAULT).createIndex("test1", "emp_no");
983982
index("test1", """
984983
{"emp_no": 1}""");
@@ -997,7 +996,11 @@ public void testIntegerShortConflict() throws IOException {
997996
);
998997

999998
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
1000-
assertResultMap(result, List.of(columnInfo("emp_no", "unsupported")), List.of(matchesList().item(null), matchesList().item(null)));
999+
assertResultMap(
1000+
result,
1001+
List.of(unsupportedColumnInfo("emp_no", "integer", "short")),
1002+
List.of(matchesList().item(null), matchesList().item(null))
1003+
);
10011004
}
10021005

10031006
/**
@@ -1023,10 +1026,7 @@ public void testIntegerShortConflict() throws IOException {
10231026
* </pre>.
10241027
*/
10251028
public void testTypeConflictInObject() throws IOException {
1026-
assumeTrue(
1027-
"order of fields in error message inconsistent before 8.14",
1028-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
1029-
);
1029+
assumeOriginalTypesReported();
10301030
createIndex("test1", empNoInObject("integer"));
10311031
index("test1", """
10321032
{"foo": {"emp_no": 1}}""");
@@ -1035,7 +1035,10 @@ public void testTypeConflictInObject() throws IOException {
10351035
{"foo": {"emp_no": "cat"}}""");
10361036

10371037
Map<String, Object> result = runEsql("FROM test* | LIMIT 3");
1038-
assertMap(result, getResultMatcher(result).entry("columns", List.of(columnInfo("foo.emp_no", "unsupported"))).extraOk());
1038+
assertMap(
1039+
result,
1040+
getResultMatcher(result).entry("columns", List.of(unsupportedColumnInfo("foo.emp_no", "integer", "keyword"))).extraOk()
1041+
);
10391042

10401043
ResponseException e = expectThrows(ResponseException.class, () -> runEsql("FROM test* | SORT foo.emp_no | LIMIT 3"));
10411044
String err = EntityUtils.toString(e.getResponse().getEntity());
@@ -1339,6 +1342,12 @@ private void assumeIndexResolverNestedFieldsNameClashFixed() throws IOException
13391342
);
13401343
}
13411344

1345+
private void assumeOriginalTypesReported() throws IOException {
1346+
var capsName = EsqlCapabilities.Cap.REPORT_ORIGINAL_TYPES.name().toLowerCase(Locale.ROOT);
1347+
boolean requiredClusterCapability = clusterHasCapability("POST", "/_query", List.of(), List.of(capsName)).orElse(false);
1348+
assumeTrue("This test makes sense for versions that report original types", requiredClusterCapability);
1349+
}
1350+
13421351
private CheckedConsumer<XContentBuilder, IOException> empNoInObject(String empNoType) {
13431352
return index -> {
13441353
index.startObject("properties");
@@ -1672,6 +1681,10 @@ private static Map<String, Object> columnInfo(String name, String type) {
16721681
return Map.of("name", name, "type", type);
16731682
}
16741683

1684+
private static Map<String, Object> unsupportedColumnInfo(String name, String... originalTypes) {
1685+
return Map.of("name", name, "type", "unsupported", "original_types", List.of(originalTypes));
1686+
}
1687+
16751688
private static void index(String name, String... docs) throws IOException {
16761689
Request request = new Request("POST", "/" + name + "/_bulk");
16771690
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)