Skip to content

Commit f65e4d6

Browse files
authored
SQL: add support for index aliases for SYS COLUMNS command (elastic#53525)
1 parent b67863e commit f65e4d6

File tree

6 files changed

+656
-58
lines changed

6 files changed

+656
-58
lines changed

x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/index/IndexResolver.java

Lines changed: 215 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package org.elasticsearch.xpack.ql.index;
77

88
import com.carrotsearch.hppc.cursors.ObjectCursor;
9+
import com.carrotsearch.hppc.cursors.ObjectObjectCursor;
910

1011
import org.elasticsearch.ElasticsearchSecurityException;
1112
import org.elasticsearch.action.ActionListener;
@@ -16,12 +17,14 @@
1617
import org.elasticsearch.action.admin.indices.get.GetIndexResponse;
1718
import org.elasticsearch.action.fieldcaps.FieldCapabilities;
1819
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest;
20+
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse;
1921
import org.elasticsearch.action.support.IndicesOptions;
2022
import org.elasticsearch.action.support.IndicesOptions.Option;
2123
import org.elasticsearch.action.support.IndicesOptions.WildcardStates;
2224
import org.elasticsearch.client.Client;
2325
import org.elasticsearch.cluster.metadata.AliasMetaData;
2426
import org.elasticsearch.common.Strings;
27+
import org.elasticsearch.common.collect.ImmutableOpenMap;
2528
import org.elasticsearch.index.IndexNotFoundException;
2629
import org.elasticsearch.index.IndexSettings;
2730
import org.elasticsearch.xpack.ql.QlIllegalArgumentException;
@@ -42,9 +45,11 @@
4245
import java.util.Collections;
4346
import java.util.Comparator;
4447
import java.util.EnumSet;
48+
import java.util.HashMap;
4549
import java.util.HashSet;
4650
import java.util.Iterator;
4751
import java.util.LinkedHashMap;
52+
import java.util.LinkedHashSet;
4853
import java.util.List;
4954
import java.util.Map;
5055
import java.util.Map.Entry;
@@ -164,7 +169,6 @@ public boolean equals(Object obj) {
164169
private final String clusterName;
165170
private final DataTypeRegistry typeRegistry;
166171

167-
168172
public IndexResolver(Client client, String clusterName, DataTypeRegistry typeRegistry) {
169173
this.client = client;
170174
this.clusterName = clusterName;
@@ -296,7 +300,7 @@ public static IndexResolution mergedMappings(DataTypeRegistry typeRegistry, Stri
296300
}
297301

298302
// merge all indices onto the same one
299-
List<EsIndex> indices = buildIndices(typeRegistry, indexNames, null, fieldCaps, i -> indexPattern, (n, types) -> {
303+
List<EsIndex> indices = buildIndices(typeRegistry, indexNames, null, fieldCaps, null, i -> indexPattern, (n, types) -> {
300304
StringBuilder errorMessage = new StringBuilder();
301305

302306
boolean hasUnmapped = types.containsKey(UNMAPPED);
@@ -473,17 +477,32 @@ private static FieldCapabilitiesRequest createFieldCapsRequest(String index, boo
473477
public void resolveAsSeparateMappings(String indexWildcard, String javaRegex, boolean includeFrozen,
474478
ActionListener<List<EsIndex>> listener) {
475479
FieldCapabilitiesRequest fieldRequest = createFieldCapsRequest(indexWildcard, includeFrozen);
476-
client.fieldCaps(fieldRequest,
477-
ActionListener.wrap(
478-
response -> listener.onResponse(
479-
separateMappings(typeRegistry, indexWildcard, javaRegex, response.getIndices(), response.get())),
480-
listener::onFailure));
480+
client.fieldCaps(fieldRequest, wrap(response -> {
481+
client.admin().indices().getAliases(createGetAliasesRequest(response, includeFrozen), wrap(aliases ->
482+
listener.onResponse(separateMappings(typeRegistry, javaRegex, response.getIndices(), response.get(), aliases.getAliases())),
483+
ex -> {
484+
if (ex instanceof IndexNotFoundException || ex instanceof ElasticsearchSecurityException) {
485+
listener.onResponse(separateMappings(typeRegistry, javaRegex, response.getIndices(), response.get(), null));
486+
} else {
487+
listener.onFailure(ex);
488+
}
489+
}));
490+
},
491+
listener::onFailure));
481492

482493
}
483494

484-
public static List<EsIndex> separateMappings(DataTypeRegistry typeRegistry, String indexPattern, String javaRegex, String[] indexNames,
485-
Map<String, Map<String, FieldCapabilities>> fieldCaps) {
486-
return buildIndices(typeRegistry, indexNames, javaRegex, fieldCaps, Function.identity(), (s, cap) -> null);
495+
private GetAliasesRequest createGetAliasesRequest(FieldCapabilitiesResponse response, boolean includeFrozen) {
496+
return new GetAliasesRequest()
497+
.local(true)
498+
.aliases("*")
499+
.indices(response.getIndices())
500+
.indicesOptions(includeFrozen ? FIELD_CAPS_FROZEN_INDICES_OPTIONS : FIELD_CAPS_INDICES_OPTIONS);
501+
}
502+
503+
public static List<EsIndex> separateMappings(DataTypeRegistry typeRegistry, String javaRegex, String[] indexNames,
504+
Map<String, Map<String, FieldCapabilities>> fieldCaps, ImmutableOpenMap<String, List<AliasMetaData>> aliases) {
505+
return buildIndices(typeRegistry, indexNames, javaRegex, fieldCaps, aliases, Function.identity(), (s, cap) -> null);
487506
}
488507

489508
private static class Fields {
@@ -496,16 +515,27 @@ private static class Fields {
496515
* each field.
497516
*/
498517
private static List<EsIndex> buildIndices(DataTypeRegistry typeRegistry, String[] indexNames, String javaRegex,
499-
Map<String, Map<String, FieldCapabilities>> fieldCaps,
518+
Map<String, Map<String, FieldCapabilities>> fieldCaps, ImmutableOpenMap<String, List<AliasMetaData>> aliases,
500519
Function<String, String> indexNameProcessor,
501520
BiFunction<String, Map<String, FieldCapabilities>, InvalidMappedField> validityVerifier) {
502521

503-
if (indexNames == null || indexNames.length == 0) {
522+
if ((indexNames == null || indexNames.length == 0) && (aliases == null || aliases.isEmpty())) {
504523
return emptyList();
505524
}
506525

507-
final List<String> resolvedIndices = asList(indexNames);
508-
Map<String, Fields> indices = new LinkedHashMap<>(resolvedIndices.size());
526+
Set<String> resolvedAliases = new HashSet<>();
527+
if (aliases != null) {
528+
Iterator<ObjectObjectCursor<String, List<AliasMetaData>>> iterator = aliases.iterator();
529+
while (iterator.hasNext()) {
530+
for (AliasMetaData alias : iterator.next().value) {
531+
resolvedAliases.add(alias.getAlias());
532+
}
533+
}
534+
}
535+
536+
List<String> resolvedIndices = new ArrayList<>(asList(indexNames));
537+
int mapSize = CollectionUtils.mapSize(resolvedIndices.size() + resolvedAliases.size());
538+
Map<String, Fields> indices = new LinkedHashMap<>(mapSize);
509539
Pattern pattern = javaRegex != null ? Pattern.compile(javaRegex) : null;
510540

511541
// sort fields in reverse order to build the field hierarchy
@@ -525,6 +555,8 @@ private static List<EsIndex> buildIndices(DataTypeRegistry typeRegistry, String[
525555
Map<String, FieldCapabilities> types = new LinkedHashMap<>(entry.getValue());
526556
// apply verification and possibly remove the "duplicate" CONSTANT_KEYWORD field type
527557
final InvalidMappedField invalidField = validityVerifier.apply(fieldName, types);
558+
// apply verification for fields belonging to index aliases
559+
Map<String, InvalidMappedField> invalidFieldsForAliases = getInvalidFieldsForAliases(fieldName, types, aliases);
528560

529561
// filter meta fields and unmapped
530562
FieldCapabilities unmapped = types.get(UNMAPPED);
@@ -545,7 +577,7 @@ private static List<EsIndex> buildIndices(DataTypeRegistry typeRegistry, String[
545577
List<String> concreteIndices = null;
546578
if (capIndices != null) {
547579
if (unmappedIndices.isEmpty()) {
548-
concreteIndices = asList(capIndices);
580+
concreteIndices = new ArrayList<>(asList(capIndices));
549581
} else {
550582
concreteIndices = new ArrayList<>(capIndices.length);
551583
for (String capIndex : capIndices) {
@@ -559,38 +591,63 @@ private static List<EsIndex> buildIndices(DataTypeRegistry typeRegistry, String[
559591
concreteIndices = resolvedIndices;
560592
}
561593

594+
// add to the list of concrete indices the aliases associated with these indices
595+
Set<String> uniqueAliases = new LinkedHashSet<>();
596+
if (aliases != null) {
597+
for (String concreteIndex : concreteIndices) {
598+
if (aliases.containsKey(concreteIndex)) {
599+
List<AliasMetaData> concreteIndexAliases = aliases.get(concreteIndex);
600+
concreteIndexAliases.stream().forEach(e -> uniqueAliases.add(e.alias()));
601+
}
602+
}
603+
concreteIndices.addAll(uniqueAliases);
604+
}
605+
562606
// put the field in their respective mappings
563607
for (String index : concreteIndices) {
564-
if (pattern == null || pattern.matcher(index).matches()) {
565-
String indexName = indexNameProcessor.apply(index);
608+
boolean isIndexAlias = uniqueAliases.contains(index);
609+
if (pattern == null || pattern.matcher(index).matches() || isIndexAlias) {
610+
String indexName = isIndexAlias ? index : indexNameProcessor.apply(index);
566611
Fields indexFields = indices.get(indexName);
567612
if (indexFields == null) {
568613
indexFields = new Fields();
569614
indices.put(indexName, indexFields);
570615
}
571616
EsField field = indexFields.flattedMapping.get(fieldName);
572-
if (field == null || (invalidField != null && (field instanceof InvalidMappedField) == false)) {
617+
boolean createField = false;
618+
if (isIndexAlias == false) {
619+
if (field == null || (invalidField != null && (field instanceof InvalidMappedField) == false)) {
620+
createField = true;
621+
}
622+
}
623+
else {
624+
if (field == null && invalidFieldsForAliases.get(index) == null) {
625+
createField = true;
626+
}
627+
}
628+
629+
if (createField) {
573630
int dot = fieldName.lastIndexOf('.');
574631
/*
575632
* Looking up the "tree" at the parent fields here to see if the field is an alias.
576633
* When the upper elements of the "tree" have no elements in fieldcaps, then this is an alias field. But not
577634
* always: if there are two aliases - a.b.c.alias1 and a.b.c.alias2 - only one of them will be considered alias.
578635
*/
579-
Holder<Boolean> isAlias = new Holder<>(false);
636+
Holder<Boolean> isAliasFieldType = new Holder<>(false);
580637
if (dot >= 0) {
581638
String parentName = fieldName.substring(0, dot);
582639
if (indexFields.flattedMapping.get(parentName) == null) {
583640
// lack of parent implies the field is an alias
584641
if (fieldCaps.get(parentName) == null) {
585-
isAlias.set(true);
642+
isAliasFieldType.set(true);
586643
}
587644
}
588645
}
589646

590647
createField(typeRegistry, fieldName, fieldCaps, indexFields.hierarchicalMapping, indexFields.flattedMapping,
591648
s -> invalidField != null ? invalidField :
592649
createField(typeRegistry, s, typeCap.getType(), emptyMap(), typeCap.isAggregatable(),
593-
isAlias.get()));
650+
isAliasFieldType.get()));
594651
}
595652
}
596653
}
@@ -605,4 +662,141 @@ private static List<EsIndex> buildIndices(DataTypeRegistry typeRegistry, String[
605662
foundIndices.sort(Comparator.comparing(EsIndex::name));
606663
return foundIndices;
607664
}
665+
666+
667+
/*
668+
* Checks if the field is valid (same type and same capabilities - searchable/aggregatable) across indices belonging to a list
669+
* of aliases.
670+
* A field can look like the example below (generated by field_caps API).
671+
* "name": {
672+
* "text": {
673+
* "type": "text",
674+
* "searchable": false,
675+
* "aggregatable": false,
676+
* "indices": [
677+
* "bar",
678+
* "foo"
679+
* ],
680+
* "non_searchable_indices": [
681+
* "foo"
682+
* ]
683+
* },
684+
* "keyword": {
685+
* "type": "keyword",
686+
* "searchable": false,
687+
* "aggregatable": true,
688+
* "non_aggregatable_indices": [
689+
* "bar", "baz"
690+
* ]
691+
* }
692+
* }
693+
*/
694+
private static Map<String, InvalidMappedField> getInvalidFieldsForAliases(String fieldName, Map<String, FieldCapabilities> types,
695+
ImmutableOpenMap<String, List<AliasMetaData>> aliases) {
696+
if (aliases == null || aliases.isEmpty()) {
697+
return emptyMap();
698+
}
699+
Map<String, InvalidMappedField> invalidFields = new HashMap<>();
700+
Map<String, Set<String>> typesErrors = new HashMap<>(); // map holding aliases and a list of unique field types across its indices
701+
Map<String, Set<String>> aliasToIndices = new HashMap<>(); // map with aliases and their list of indices
702+
703+
Iterator<ObjectObjectCursor<String, List<AliasMetaData>>> iter = aliases.iterator();
704+
while (iter.hasNext()) {
705+
ObjectObjectCursor<String, List<AliasMetaData>> index = iter.next();
706+
for (AliasMetaData aliasMetaData : index.value) {
707+
String aliasName = aliasMetaData.alias();
708+
aliasToIndices.putIfAbsent(aliasName, new HashSet<>());
709+
aliasToIndices.get(aliasName).add(index.key);
710+
}
711+
}
712+
713+
// iterate over each type
714+
for (Entry<String, FieldCapabilities> type : types.entrySet()) {
715+
String esFieldType = type.getKey();
716+
if (esFieldType == UNMAPPED) {
717+
continue;
718+
}
719+
String[] indices = type.getValue().indices();
720+
// if there is a list of indices where this field type is defined
721+
if (indices != null) {
722+
// Look at all these indices' aliases and add the type of the field to a list (Set) with unique elements.
723+
// A valid mapping for a field in an index alias should contain only one type. If it doesn't, this means that field
724+
// is mapped as different types across the indices in this index alias.
725+
for (String index : indices) {
726+
List<AliasMetaData> indexAliases = aliases.get(index);
727+
if (indexAliases == null) {
728+
continue;
729+
}
730+
for (AliasMetaData aliasMetaData : indexAliases) {
731+
String aliasName = aliasMetaData.alias();
732+
if (typesErrors.containsKey(aliasName)) {
733+
typesErrors.get(aliasName).add(esFieldType);
734+
} else {
735+
Set<String> fieldTypes = new HashSet<>();
736+
fieldTypes.add(esFieldType);
737+
typesErrors.put(aliasName, fieldTypes);
738+
}
739+
}
740+
}
741+
}
742+
}
743+
744+
for (String aliasName : aliasToIndices.keySet()) {
745+
// if, for the same index alias, there are multiple field types for this fieldName ie the index alias has indices where the same
746+
// field name is of different types
747+
Set<String> esFieldTypes = typesErrors.get(aliasName);
748+
if (esFieldTypes != null && esFieldTypes.size() > 1) {
749+
// consider the field as invalid, for the currently checked index alias
750+
// the error message doesn't actually matter
751+
invalidFields.put(aliasName, new InvalidMappedField(fieldName));
752+
} else {
753+
// if the field type is the same across all this alias' indices, check the field's capabilities (searchable/aggregatable)
754+
for (Entry<String, FieldCapabilities> type : types.entrySet()) {
755+
if (type.getKey() == UNMAPPED) {
756+
continue;
757+
}
758+
FieldCapabilities f = type.getValue();
759+
760+
// the existence of a list of non_aggregatable_indices is an indication that not all indices have the same capabilities
761+
// but this list can contain indices belonging to other aliases, so we need to check only for this alias
762+
if (f.nonAggregatableIndices() != null) {
763+
Set<String> aliasIndices = aliasToIndices.get(aliasName);
764+
int nonAggregatableCount = 0;
765+
// either all or none of the non-aggregatable indices belonging to a certain alias should be in this list
766+
for (String nonAggIndex : f.nonAggregatableIndices()) {
767+
if (aliasIndices.contains(nonAggIndex)) {
768+
nonAggregatableCount++;
769+
}
770+
}
771+
if (nonAggregatableCount > 0 && nonAggregatableCount != aliasIndices.size()) {
772+
invalidFields.put(aliasName, new InvalidMappedField(fieldName));
773+
break;
774+
}
775+
}
776+
777+
// perform the same check for non_searchable_indices list
778+
if (f.nonSearchableIndices() != null) {
779+
Set<String> aliasIndices = aliasToIndices.get(aliasName);
780+
int nonSearchableCount = 0;
781+
// either all or none of the non-searchable indices belonging to a certain alias should be in this list
782+
for (String nonSearchIndex : f.nonSearchableIndices()) {
783+
if (aliasIndices.contains(nonSearchIndex)) {
784+
nonSearchableCount++;
785+
}
786+
}
787+
if (nonSearchableCount > 0 && nonSearchableCount != aliasIndices.size()) {
788+
invalidFields.put(aliasName, new InvalidMappedField(fieldName));
789+
break;
790+
}
791+
}
792+
}
793+
}
794+
}
795+
796+
if (invalidFields.size() > 0) {
797+
return invalidFields;
798+
}
799+
// everything checks
800+
return emptyMap();
801+
}
608802
}

x-pack/plugin/ql/src/main/java/org/elasticsearch/xpack/ql/type/InvalidMappedField.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ public InvalidMappedField(String name, String errorMessage) {
2525
this.errorMessage = errorMessage;
2626
}
2727

28+
public InvalidMappedField(String name) {
29+
super(name, DataTypes.UNSUPPORTED, emptyMap(), false);
30+
this.errorMessage = StringUtils.EMPTY;
31+
}
32+
2833
public String errorMessage() {
2934
return errorMessage;
3035
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.sql.qa.single_node;
8+
9+
import org.elasticsearch.xpack.sql.qa.jdbc.SysColumnsTestCase;
10+
11+
public class SysColumnsIT extends SysColumnsTestCase {
12+
13+
}

0 commit comments

Comments
 (0)