Skip to content

Commit d95121e

Browse files
committed
ESQL: Report original_types
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 d8b067d commit d95121e

File tree

22 files changed

+367
-206
lines changed

22 files changed

+367
-206
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ static TransportVersion def(int id) {
185185
public static final TransportVersion ESQL_THREAD_NAME_IN_DRIVER_PROFILE = def(9_027_0_00);
186186
public static final TransportVersion INFERENCE_CONTEXT = def(9_028_0_00);
187187
public static final TransportVersion ML_INFERENCE_DEEPSEEK = def(9_029_00_0);
188+
public static final TransportVersion ESQL_REPORT_ORIGINAL_TYPES = def(9_030_00_0);
188189

189190
/*
190191
* 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
@@ -323,12 +323,13 @@ public void testAliasToInt() throws IOException {
323323
}
324324

325325
public void testFlattenedUnsupported() throws IOException {
326+
assumeOriginalTypesReported();
326327
new Test("flattened").createIndex("test", "flattened");
327328
index("test", """
328329
{"flattened": {"a": "foo"}}""");
329330
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
330331

331-
assertResultMap(result, List.of(columnInfo("flattened", "unsupported")), List.of(matchesList().item(null)));
332+
assertResultMap(result, List.of(unsupportedColumnInfo("flattened", "flattened")), List.of(matchesList().item(null)));
332333
}
333334

334335
public void testEmptyMapping() throws IOException {
@@ -685,6 +686,7 @@ public void testByteFieldWithIntSubfieldTooBig() throws IOException {
685686
* </pre>.
686687
*/
687688
public void testIncompatibleTypes() throws IOException {
689+
assumeOriginalTypesReported();
688690
keywordTest().createIndex("test1", "f");
689691
index("test1", """
690692
{"f": "f1"}""");
@@ -693,7 +695,11 @@ public void testIncompatibleTypes() throws IOException {
693695
{"f": 1}""");
694696

695697
Map<String, Object> result = runEsql("FROM test*");
696-
assertResultMap(result, List.of(columnInfo("f", "unsupported")), List.of(matchesList().item(null), matchesList().item(null)));
698+
assertResultMap(
699+
result,
700+
List.of(unsupportedColumnInfo("f", "keyword", "long")),
701+
List.of(matchesList().item(null), matchesList().item(null))
702+
);
697703
ResponseException e = expectThrows(ResponseException.class, () -> runEsql("FROM test* | SORT f | LIMIT 3"));
698704
String err = EntityUtils.toString(e.getResponse().getEntity());
699705
assertThat(
@@ -754,10 +760,7 @@ public void testDistinctInEachIndex() throws IOException {
754760
* </pre>.
755761
*/
756762
public void testMergeKeywordAndObject() throws IOException {
757-
assumeTrue(
758-
"order of fields in error message inconsistent before 8.14",
759-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
760-
);
763+
assumeOriginalTypesReported();
761764
keywordTest().createIndex("test1", "file");
762765
index("test1", """
763766
{"file": "f1"}""");
@@ -793,7 +796,7 @@ public void testMergeKeywordAndObject() throws IOException {
793796
Map<String, Object> result = runEsql("FROM test* | SORT file.raw | LIMIT 2");
794797
assertResultMap(
795798
result,
796-
List.of(columnInfo("file", "unsupported"), columnInfo("file.raw", "keyword")),
799+
List.of(unsupportedColumnInfo("file", "keyword", "object"), columnInfo("file.raw", "keyword")),
797800
List.of(matchesList().item(null).item("o2"), matchesList().item(null).item(null))
798801
);
799802
}
@@ -813,6 +816,7 @@ public void testMergeKeywordAndObject() throws IOException {
813816
* </pre>.
814817
*/
815818
public void testPropagateUnsupportedToSubFields() throws IOException {
819+
assumeOriginalTypesReported();
816820
createIndex("test", index -> {
817821
index.startObject("properties");
818822
index.startObject("f");
@@ -838,7 +842,7 @@ public void testPropagateUnsupportedToSubFields() throws IOException {
838842
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
839843
assertResultMap(
840844
result,
841-
List.of(columnInfo("f", "unsupported"), columnInfo("f.raw", "unsupported")),
845+
List.of(unsupportedColumnInfo("f", "ip_range"), unsupportedColumnInfo("f.raw", "ip_range")),
842846
List.of(matchesList().item(null).item(null))
843847
);
844848
}
@@ -863,10 +867,7 @@ public void testPropagateUnsupportedToSubFields() throws IOException {
863867
* </pre>.
864868
*/
865869
public void testMergeUnsupportedAndObject() throws IOException {
866-
assumeTrue(
867-
"order of fields in error message inconsistent before 8.14",
868-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
869-
);
870+
assumeOriginalTypesReported();
870871
createIndex("test1", index -> {
871872
index.startObject("properties");
872873
index.startObject("f").field("type", "ip_range").endObject();
@@ -901,7 +902,7 @@ public void testMergeUnsupportedAndObject() throws IOException {
901902
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
902903
assertResultMap(
903904
result,
904-
List.of(columnInfo("f", "unsupported"), columnInfo("f.raw", "unsupported")),
905+
List.of(unsupportedColumnInfo("f", "ip_range"), unsupportedColumnInfo("f.raw", "ip_range")),
905906
List.of(matchesList().item(null).item(null), matchesList().item(null).item(null))
906907
);
907908
}
@@ -954,10 +955,7 @@ public void testIntegerDocValuesConflict() throws IOException {
954955
* In an ideal world we'd promote the {@code integer} to an {@code long} and just go.
955956
*/
956957
public void testLongIntegerConflict() throws IOException {
957-
assumeTrue(
958-
"order of fields in error message inconsistent before 8.14",
959-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
960-
);
958+
assumeOriginalTypesReported();
961959
longTest().sourceMode(SourceMode.DEFAULT).createIndex("test1", "emp_no");
962960
index("test1", """
963961
{"emp_no": 1}""");
@@ -976,7 +974,11 @@ public void testLongIntegerConflict() throws IOException {
976974
);
977975

978976
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
979-
assertResultMap(result, List.of(columnInfo("emp_no", "unsupported")), List.of(matchesList().item(null), matchesList().item(null)));
977+
assertResultMap(
978+
result,
979+
List.of(unsupportedColumnInfo("emp_no", "integer", "long")),
980+
List.of(matchesList().item(null), matchesList().item(null))
981+
);
980982
}
981983

982984
/**
@@ -996,10 +998,7 @@ public void testLongIntegerConflict() throws IOException {
996998
* In an ideal world we'd promote the {@code short} to an {@code integer} and just go.
997999
*/
9981000
public void testIntegerShortConflict() throws IOException {
999-
assumeTrue(
1000-
"order of fields in error message inconsistent before 8.14",
1001-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
1002-
);
1001+
assumeOriginalTypesReported();
10031002
intTest().sourceMode(SourceMode.DEFAULT).createIndex("test1", "emp_no");
10041003
index("test1", """
10051004
{"emp_no": 1}""");
@@ -1018,7 +1017,11 @@ public void testIntegerShortConflict() throws IOException {
10181017
);
10191018

10201019
Map<String, Object> result = runEsql("FROM test* | LIMIT 2");
1021-
assertResultMap(result, List.of(columnInfo("emp_no", "unsupported")), List.of(matchesList().item(null), matchesList().item(null)));
1020+
assertResultMap(
1021+
result,
1022+
List.of(unsupportedColumnInfo("emp_no", "integer", "short")),
1023+
List.of(matchesList().item(null), matchesList().item(null))
1024+
);
10221025
}
10231026

10241027
/**
@@ -1044,10 +1047,7 @@ public void testIntegerShortConflict() throws IOException {
10441047
* </pre>.
10451048
*/
10461049
public void testTypeConflictInObject() throws IOException {
1047-
assumeTrue(
1048-
"order of fields in error message inconsistent before 8.14",
1049-
getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0))
1050-
);
1050+
assumeOriginalTypesReported();
10511051
createIndex("test1", empNoInObject("integer"));
10521052
index("test1", """
10531053
{"foo": {"emp_no": 1}}""");
@@ -1056,7 +1056,10 @@ public void testTypeConflictInObject() throws IOException {
10561056
{"foo": {"emp_no": "cat"}}""");
10571057

10581058
Map<String, Object> result = runEsql("FROM test* | LIMIT 3");
1059-
assertMap(result, getResultMatcher(result).entry("columns", List.of(columnInfo("foo.emp_no", "unsupported"))).extraOk());
1059+
assertMap(
1060+
result,
1061+
getResultMatcher(result).entry("columns", List.of(unsupportedColumnInfo("foo.emp_no", "integer", "keyword"))).extraOk()
1062+
);
10601063

10611064
ResponseException e = expectThrows(ResponseException.class, () -> runEsql("FROM test* | SORT foo.emp_no | LIMIT 3"));
10621065
String err = EntityUtils.toString(e.getResponse().getEntity());
@@ -1366,6 +1369,12 @@ private void assumeIndexResolverNestedFieldsNameClashFixed() throws IOException
13661369
);
13671370
}
13681371

1372+
private void assumeOriginalTypesReported() throws IOException {
1373+
var capsName = EsqlCapabilities.Cap.REPORT_ORIGINAL_TYPES.name().toLowerCase(Locale.ROOT);
1374+
boolean requiredClusterCapability = clusterHasCapability("POST", "/_query", List.of(), List.of(capsName)).orElse(false);
1375+
assumeTrue("This test makes sense for versions that report original types", requiredClusterCapability);
1376+
}
1377+
13691378
private CheckedConsumer<XContentBuilder, IOException> empNoInObject(String empNoType) {
13701379
return index -> {
13711380
index.startObject("properties");
@@ -1701,6 +1710,10 @@ private static Map<String, Object> columnInfo(String name, String type) {
17011710
return Map.of("name", name, "type", type);
17021711
}
17031712

1713+
private static Map<String, Object> unsupportedColumnInfo(String name, String... originalTypes) {
1714+
return Map.of("name", name, "type", "unsupported", "original_types", List.of(originalTypes));
1715+
}
1716+
17041717
private static void index(String name, String... docs) throws IOException {
17051718
Request request = new Request("POST", "/" + name + "/_bulk");
17061719
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 {

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import static org.elasticsearch.xpack.esql.action.EsqlAsyncTestUtils.waitForCluster;
3232
import static org.hamcrest.Matchers.equalTo;
3333
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
34+
import static org.hamcrest.Matchers.hasItem;
3435
import static org.hamcrest.Matchers.is;
3536
import static org.hamcrest.Matchers.lessThanOrEqualTo;
3637
import static org.hamcrest.Matchers.not;
@@ -151,10 +152,10 @@ public void testAsyncQueriesWithLimit0() throws IOException {
151152

152153
} else {
153154
assertThat(resp.columns().size(), equalTo(4));
154-
assertThat(resp.columns().contains(new ColumnInfoImpl("const", "long")), is(true));
155-
assertThat(resp.columns().contains(new ColumnInfoImpl("id", "keyword")), is(true));
156-
assertThat(resp.columns().contains(new ColumnInfoImpl("tag", "keyword")), is(true));
157-
assertThat(resp.columns().contains(new ColumnInfoImpl("v", "long")), is(true));
155+
assertThat(resp.columns(), hasItem(new ColumnInfoImpl("const", "long", null)));
156+
assertThat(resp.columns(), hasItem(new ColumnInfoImpl("id", "keyword", null)));
157+
assertThat(resp.columns(), hasItem(new ColumnInfoImpl("tag", "keyword", null)));
158+
assertThat(resp.columns(), hasItem(new ColumnInfoImpl("v", "long", null)));
158159
assertThat(resp.values().hasNext(), is(false)); // values should be empty list
159160

160161
assertNotNull(executionInfo);

0 commit comments

Comments
 (0)