|
27 | 27 | import org.elasticsearch.xcontent.XContentBuilder;
|
28 | 28 | import org.elasticsearch.xcontent.XContentType;
|
29 | 29 | import org.elasticsearch.xcontent.json.JsonXContent;
|
| 30 | +import org.elasticsearch.xpack.esql.core.type.DataType; |
30 | 31 | import org.elasticsearch.xpack.esql.qa.rest.RestEsqlTestCase;
|
31 | 32 | import org.elasticsearch.xpack.esql.tools.ProfileParser;
|
32 | 33 | import org.hamcrest.Matchers;
|
|
40 | 41 | import java.nio.charset.StandardCharsets;
|
41 | 42 | import java.util.ArrayList;
|
42 | 43 | import java.util.Arrays;
|
| 44 | +import java.util.Comparator; |
43 | 45 | import java.util.HashSet;
|
44 | 46 | import java.util.List;
|
45 | 47 | import java.util.Locale;
|
46 | 48 | import java.util.Map;
|
47 | 49 | import java.util.Set;
|
| 50 | +import java.util.stream.Collectors; |
| 51 | +import java.util.stream.Stream; |
48 | 52 |
|
49 | 53 | import static org.elasticsearch.test.ListMatcher.matchesList;
|
50 | 54 | import static org.elasticsearch.test.MapMatcher.assertMap;
|
@@ -648,6 +652,120 @@ public void testForceSleepsProfile() throws IOException {
|
648 | 652 | }
|
649 | 653 | }
|
650 | 654 |
|
| 655 | + public void testSuggestedCast() throws IOException { |
| 656 | + // TODO: Figure out how best to make sure we don't leave out new types |
| 657 | + Map<DataType, String> typesAndValues = Map.ofEntries( |
| 658 | + Map.entry(DataType.BOOLEAN, "\"true\""), |
| 659 | + Map.entry(DataType.LONG, "-1234567890234567"), |
| 660 | + Map.entry(DataType.INTEGER, "123"), |
| 661 | + Map.entry(DataType.UNSIGNED_LONG, "1234567890234567"), |
| 662 | + Map.entry(DataType.DOUBLE, "12.4"), |
| 663 | + Map.entry(DataType.KEYWORD, "\"keyword\""), |
| 664 | + Map.entry(DataType.TEXT, "\"some text\""), |
| 665 | + Map.entry(DataType.DATE_NANOS, "\"2015-01-01T12:10:30.123456789Z\""), |
| 666 | + Map.entry(DataType.DATETIME, "\"2015-01-01T12:10:30Z\""), |
| 667 | + Map.entry(DataType.IP, "\"192.168.30.1\""), |
| 668 | + Map.entry(DataType.VERSION, "\"8.19.0\""), |
| 669 | + Map.entry(DataType.GEO_POINT, "[-71.34, 41.12]"), |
| 670 | + Map.entry(DataType.GEO_SHAPE, """ |
| 671 | + { |
| 672 | + "type": "Point", |
| 673 | + "coordinates": [-77.03653, 38.897676] |
| 674 | + } |
| 675 | + """), |
| 676 | + Map.entry(DataType.AGGREGATE_METRIC_DOUBLE, """ |
| 677 | + { |
| 678 | + "max": 14983.1 |
| 679 | + } |
| 680 | + """) |
| 681 | + ); |
| 682 | + Set<DataType> shouldBeSupported = Stream.of(DataType.values()).filter(DataType::isRepresentable).collect(Collectors.toSet()); |
| 683 | + shouldBeSupported.remove(DataType.CARTESIAN_POINT); |
| 684 | + shouldBeSupported.remove(DataType.CARTESIAN_SHAPE); |
| 685 | + shouldBeSupported.remove(DataType.NULL); |
| 686 | + shouldBeSupported.remove(DataType.DOC_DATA_TYPE); |
| 687 | + shouldBeSupported.remove(DataType.TSID_DATA_TYPE); |
| 688 | + for (DataType type : shouldBeSupported) { |
| 689 | + assertTrue(typesAndValues.containsKey(type)); |
| 690 | + } |
| 691 | + assertThat(typesAndValues.size(), equalTo(shouldBeSupported.size())); |
| 692 | + |
| 693 | + for (DataType type : typesAndValues.keySet()) { |
| 694 | + String additionalProperties = ""; |
| 695 | + if (type == DataType.AGGREGATE_METRIC_DOUBLE) { |
| 696 | + additionalProperties += """ |
| 697 | + , |
| 698 | + "metrics": ["max"], |
| 699 | + "default_metric": "max" |
| 700 | + """; |
| 701 | + } |
| 702 | + createIndex("index-" + type.esType(), null, """ |
| 703 | + "properties": { |
| 704 | + "my_field": { |
| 705 | + "type": "%s" %s |
| 706 | + } |
| 707 | + } |
| 708 | + """.formatted(type.esType(), additionalProperties)); |
| 709 | + Request doc = new Request("PUT", "index-" + type.esType() + "/_doc/1"); |
| 710 | + doc.setJsonEntity("{\"my_field\": " + typesAndValues.get(type) + "}"); |
| 711 | + client().performRequest(doc); |
| 712 | + } |
| 713 | + |
| 714 | + List<DataType> listOfTypes = new ArrayList<>(typesAndValues.keySet()); |
| 715 | + listOfTypes.sort(Comparator.comparing(DataType::typeName)); |
| 716 | + |
| 717 | + for (int i = 0; i < listOfTypes.size(); i++) { |
| 718 | + for (int j = i + 1; j < listOfTypes.size(); j++) { |
| 719 | + String query = """ |
| 720 | + { |
| 721 | + "query": "FROM index-%s,index-%s | LIMIT 100 | KEEP my_field" |
| 722 | + } |
| 723 | + """.formatted(listOfTypes.get(i).esType(), listOfTypes.get(j).esType()); |
| 724 | + Request request = new Request("POST", "/_query"); |
| 725 | + request.setJsonEntity(query); |
| 726 | + Response resp = client().performRequest(request); |
| 727 | + Map<String, Object> results = entityAsMap(resp); |
| 728 | + List<?> columns = (List<?>) results.get("columns"); |
| 729 | + DataType suggestedCast = DataType.suggestedCast(Set.of(listOfTypes.get(i), listOfTypes.get(j))); |
| 730 | + assertThat( |
| 731 | + columns, |
| 732 | + equalTo( |
| 733 | + List.of( |
| 734 | + Map.ofEntries( |
| 735 | + Map.entry("name", "my_field"), |
| 736 | + Map.entry("type", "unsupported"), |
| 737 | + Map.entry("original_types", List.of(listOfTypes.get(i).typeName(), listOfTypes.get(j).typeName())), |
| 738 | + Map.entry("suggested_cast", suggestedCast.typeName()) |
| 739 | + ) |
| 740 | + ) |
| 741 | + ) |
| 742 | + ); |
| 743 | + |
| 744 | + String castedQuery = """ |
| 745 | + { |
| 746 | + "query": "FROM index-%s,index-%s | LIMIT 100 | EVAL my_field = my_field::%s" |
| 747 | + } |
| 748 | + """.formatted( |
| 749 | + listOfTypes.get(i).esType(), |
| 750 | + listOfTypes.get(j).esType(), |
| 751 | + suggestedCast == DataType.KEYWORD ? "STRING" : suggestedCast.nameUpper() |
| 752 | + ); |
| 753 | + Request castedRequest = new Request("POST", "/_query"); |
| 754 | + castedRequest.setJsonEntity(castedQuery); |
| 755 | + Response castedResponse = client().performRequest(castedRequest); |
| 756 | + Map<String, Object> castedResults = entityAsMap(castedResponse); |
| 757 | + List<?> castedColumns = (List<?>) castedResults.get("columns"); |
| 758 | + assertThat( |
| 759 | + castedColumns, |
| 760 | + equalTo(List.of(Map.ofEntries(Map.entry("name", "my_field"), Map.entry("type", suggestedCast.typeName())))) |
| 761 | + ); |
| 762 | + } |
| 763 | + } |
| 764 | + for (DataType type : typesAndValues.keySet()) { |
| 765 | + deleteIndex("index-" + type.esType()); |
| 766 | + } |
| 767 | + } |
| 768 | + |
651 | 769 | static MapMatcher commonProfile() {
|
652 | 770 | return matchesMap() //
|
653 | 771 | .entry("description", any(String.class))
|
|
0 commit comments