-
Notifications
You must be signed in to change notification settings - Fork 25k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SQL: add support for index aliases for SYS COLUMNS command #53525
Changes from 3 commits
947e501
dabcccf
3aa01f3
26bf119
93d8162
80f0b63
d34271a
bf8df94
9ed8fa2
cd6c420
079be6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -6,6 +6,7 @@ | |||||
package org.elasticsearch.xpack.ql.index; | ||||||
|
||||||
import com.carrotsearch.hppc.cursors.ObjectCursor; | ||||||
import com.carrotsearch.hppc.cursors.ObjectObjectCursor; | ||||||
|
||||||
import org.elasticsearch.ElasticsearchSecurityException; | ||||||
import org.elasticsearch.action.ActionListener; | ||||||
|
@@ -22,6 +23,7 @@ | |||||
import org.elasticsearch.client.Client; | ||||||
import org.elasticsearch.cluster.metadata.AliasMetaData; | ||||||
import org.elasticsearch.common.Strings; | ||||||
import org.elasticsearch.common.collect.ImmutableOpenMap; | ||||||
import org.elasticsearch.index.IndexNotFoundException; | ||||||
import org.elasticsearch.index.IndexSettings; | ||||||
import org.elasticsearch.xpack.ql.QlIllegalArgumentException; | ||||||
|
@@ -41,9 +43,11 @@ | |||||
import java.util.Collections; | ||||||
import java.util.Comparator; | ||||||
import java.util.EnumSet; | ||||||
import java.util.HashMap; | ||||||
import java.util.HashSet; | ||||||
import java.util.Iterator; | ||||||
import java.util.LinkedHashMap; | ||||||
import java.util.LinkedHashSet; | ||||||
import java.util.List; | ||||||
import java.util.Map; | ||||||
import java.util.Map.Entry; | ||||||
|
@@ -162,7 +166,6 @@ public boolean equals(Object obj) { | |||||
private final String clusterName; | ||||||
private final DataTypeRegistry typeRegistry; | ||||||
|
||||||
|
||||||
public IndexResolver(Client client, String clusterName, DataTypeRegistry typeRegistry) { | ||||||
this.client = client; | ||||||
this.clusterName = clusterName; | ||||||
|
@@ -294,7 +297,7 @@ public static IndexResolution mergedMappings(DataTypeRegistry typeRegistry, Stri | |||||
} | ||||||
|
||||||
// merge all indices onto the same one | ||||||
List<EsIndex> indices = buildIndices(typeRegistry, indexNames, null, fieldCaps, i -> indexPattern, (n, types) -> { | ||||||
List<EsIndex> indices = buildIndices(typeRegistry, indexNames, null, fieldCaps, null, i -> indexPattern, (n, types) -> { | ||||||
StringBuilder errorMessage = new StringBuilder(); | ||||||
|
||||||
boolean hasUnmapped = types.containsKey(UNMAPPED); | ||||||
|
@@ -460,15 +463,31 @@ public void resolveAsSeparateMappings(String indexWildcard, String javaRegex, bo | |||||
FieldCapabilitiesRequest fieldRequest = createFieldCapsRequest(indexWildcard, includeFrozen); | ||||||
client.fieldCaps(fieldRequest, | ||||||
ActionListener.wrap( | ||||||
response -> listener.onResponse( | ||||||
separateMappings(typeRegistry, indexWildcard, javaRegex, response.getIndices(), response.get())), | ||||||
response -> { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small nit - maybe it makes sense to extract this method into a separate one - it's not reusable but it might make the call easier to read:
|
||||||
String[] indicesList = response.getIndices(); | ||||||
GetAliasesRequest aliasRequest = new GetAliasesRequest() | ||||||
.local(true) | ||||||
.aliases("*") | ||||||
.indices(indicesList) | ||||||
.indicesOptions(IndicesOptions.lenientExpandOpen()); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||
client.admin().indices().getAliases(aliasRequest, wrap(aliases -> | ||||||
listener.onResponse(separateMappings(typeRegistry, javaRegex, indicesList, response.get(), | ||||||
aliases.getAliases())), | ||||||
ex -> { | ||||||
if (ex instanceof IndexNotFoundException || ex instanceof ElasticsearchSecurityException) { | ||||||
listener.onResponse(separateMappings(typeRegistry, javaRegex, indicesList, response.get(), null)); | ||||||
} else { | ||||||
listener.onFailure(ex); | ||||||
} | ||||||
})); | ||||||
}, | ||||||
listener::onFailure)); | ||||||
|
||||||
} | ||||||
|
||||||
public static List<EsIndex> separateMappings(DataTypeRegistry typeRegistry, String indexPattern, String javaRegex, String[] indexNames, | ||||||
Map<String, Map<String, FieldCapabilities>> fieldCaps) { | ||||||
return buildIndices(typeRegistry, indexNames, javaRegex, fieldCaps, Function.identity(), (s, cap) -> null); | ||||||
public static List<EsIndex> separateMappings(DataTypeRegistry typeRegistry, String javaRegex, String[] indexNames, | ||||||
Map<String, Map<String, FieldCapabilities>> fieldCaps, ImmutableOpenMap<String, List<AliasMetaData>> aliases) { | ||||||
astefan marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
return buildIndices(typeRegistry, indexNames, javaRegex, fieldCaps, aliases, Function.identity(), (s, cap) -> null); | ||||||
} | ||||||
|
||||||
private static class Fields { | ||||||
|
@@ -481,16 +500,26 @@ private static class Fields { | |||||
* each field. | ||||||
*/ | ||||||
private static List<EsIndex> buildIndices(DataTypeRegistry typeRegistry, String[] indexNames, String javaRegex, | ||||||
Map<String, Map<String, FieldCapabilities>> fieldCaps, | ||||||
Map<String, Map<String, FieldCapabilities>> fieldCaps, ImmutableOpenMap<String, List<AliasMetaData>> aliases, | ||||||
astefan marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
Function<String, String> indexNameProcessor, | ||||||
BiFunction<String, Map<String, FieldCapabilities>, InvalidMappedField> validityVerifier) { | ||||||
|
||||||
if (indexNames == null || indexNames.length == 0) { | ||||||
if ((indexNames == null || indexNames.length == 0) && (aliases == null || aliases.size() == 0)) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's not possible unfortunately, because |
||||||
return emptyList(); | ||||||
} | ||||||
|
||||||
final List<String> resolvedIndices = asList(indexNames); | ||||||
Map<String, Fields> indices = new LinkedHashMap<>(resolvedIndices.size()); | ||||||
Set<String> resolvedAliases = new LinkedHashSet<>(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it okay to put all aliases in the same map? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With this I wanted to count them and create a list of indices and aliases appropriately sized. |
||||||
if (aliases != null) { | ||||||
Iterator<ObjectObjectCursor<String, List<AliasMetaData>>> iterator = aliases.iterator(); | ||||||
while (iterator.hasNext()) { | ||||||
for(AliasMetaData alias : iterator.next().value) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
resolvedAliases.add(alias.getAlias()); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
List<String> resolvedIndices = new ArrayList<>(asList(indexNames)); | ||||||
Map<String, Fields> indices = new LinkedHashMap<>(resolvedIndices.size() + resolvedAliases.size()); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The size is incorrect - use |
||||||
Pattern pattern = javaRegex != null ? Pattern.compile(javaRegex) : null; | ||||||
|
||||||
// sort fields in reverse order to build the field hierarchy | ||||||
|
@@ -508,8 +537,10 @@ private static List<EsIndex> buildIndices(DataTypeRegistry typeRegistry, String[ | |||||
continue; | ||||||
} | ||||||
|
||||||
// apply verification | ||||||
// apply verification for individual fields | ||||||
final InvalidMappedField invalidField = validityVerifier.apply(fieldName, types); | ||||||
// apply verification for fields belonging to index aliases | ||||||
Map<String, InvalidMappedField> invalidFieldsForAliases = getInvalidFieldsForAliases(fieldName, types, aliases); | ||||||
|
||||||
// filter meta fields and unmapped | ||||||
FieldCapabilities unmapped = types.get(UNMAPPED); | ||||||
|
@@ -530,7 +561,7 @@ private static List<EsIndex> buildIndices(DataTypeRegistry typeRegistry, String[ | |||||
List<String> concreteIndices = null; | ||||||
if (capIndices != null) { | ||||||
if (unmappedIndices.isEmpty()) { | ||||||
concreteIndices = asList(capIndices); | ||||||
concreteIndices = new ArrayList<>(asList(capIndices)); | ||||||
} else { | ||||||
concreteIndices = new ArrayList<>(capIndices.length); | ||||||
for (String capIndex : capIndices) { | ||||||
|
@@ -544,38 +575,63 @@ private static List<EsIndex> buildIndices(DataTypeRegistry typeRegistry, String[ | |||||
concreteIndices = resolvedIndices; | ||||||
} | ||||||
|
||||||
// add to the list of concrete indices the aliases associated with these indices | ||||||
Set<String> uniqueAliases = new LinkedHashSet<>(); | ||||||
if (aliases != null) { | ||||||
for (String concreteIndex : concreteIndices) { | ||||||
if (aliases.containsKey(concreteIndex)) { | ||||||
List<AliasMetaData> concreteIndexAliases = aliases.get(concreteIndex); | ||||||
concreteIndexAliases.stream().forEach(e -> uniqueAliases.add(e.alias())); | ||||||
} | ||||||
} | ||||||
concreteIndices.addAll(uniqueAliases); | ||||||
} | ||||||
|
||||||
// put the field in their respective mappings | ||||||
for (String index : concreteIndices) { | ||||||
if (pattern == null || pattern.matcher(index).matches()) { | ||||||
String indexName = indexNameProcessor.apply(index); | ||||||
boolean isIndexAlias = uniqueAliases.contains(index); | ||||||
if (pattern == null || pattern.matcher(index).matches() || isIndexAlias) { | ||||||
String indexName = isIndexAlias ? index : indexNameProcessor.apply(index); | ||||||
Fields indexFields = indices.get(indexName); | ||||||
if (indexFields == null) { | ||||||
indexFields = new Fields(); | ||||||
indices.put(indexName, indexFields); | ||||||
} | ||||||
EsField field = indexFields.flattedMapping.get(fieldName); | ||||||
if (field == null || (invalidField != null && (field instanceof InvalidMappedField) == false)) { | ||||||
boolean createField = false; | ||||||
if (isIndexAlias == false) { | ||||||
if (field == null || (invalidField != null && (field instanceof InvalidMappedField) == false)) { | ||||||
createField = true; | ||||||
} | ||||||
} | ||||||
else { | ||||||
if (field == null && invalidFieldsForAliases.get(index) == null) { | ||||||
createField = true; | ||||||
} | ||||||
} | ||||||
|
||||||
if (createField) { | ||||||
int dot = fieldName.lastIndexOf('.'); | ||||||
/* | ||||||
* Looking up the "tree" at the parent fields here to see if the field is an alias. | ||||||
* When the upper elements of the "tree" have no elements in fieldcaps, then this is an alias field. But not | ||||||
* always: if there are two aliases - a.b.c.alias1 and a.b.c.alias2 - only one of them will be considered alias. | ||||||
*/ | ||||||
Holder<Boolean> isAlias = new Holder<>(false); | ||||||
Holder<Boolean> isAliasFieldType = new Holder<>(false); | ||||||
if (dot >= 0) { | ||||||
String parentName = fieldName.substring(0, dot); | ||||||
if (indexFields.flattedMapping.get(parentName) == null) { | ||||||
// lack of parent implies the field is an alias | ||||||
if (fieldCaps.get(parentName) == null) { | ||||||
isAlias.set(true); | ||||||
isAliasFieldType.set(true); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
createField(typeRegistry, fieldName, fieldCaps, indexFields.hierarchicalMapping, indexFields.flattedMapping, | ||||||
s -> invalidField != null ? invalidField : | ||||||
createField(typeRegistry, s, typeCap.getType(), emptyMap(), typeCap.isAggregatable(), | ||||||
isAlias.get())); | ||||||
isAliasFieldType.get())); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
@@ -590,4 +646,134 @@ private static List<EsIndex> buildIndices(DataTypeRegistry typeRegistry, String[ | |||||
foundIndices.sort(Comparator.comparing(EsIndex::name)); | ||||||
return foundIndices; | ||||||
} | ||||||
|
||||||
|
||||||
/* | ||||||
* Checks if the field is valid (same type and same capabilities - searchable/aggregatable) across indices belonging to a list | ||||||
* of aliases. | ||||||
* A field can look like the example below (generated by field_caps API). | ||||||
* "name": { | ||||||
* "text": { | ||||||
* "type": "text", | ||||||
* "searchable": false, | ||||||
* "aggregatable": false, | ||||||
* "indices": [ | ||||||
* "bar", | ||||||
* "foo" | ||||||
* ], | ||||||
* "non_searchable_indices": [ | ||||||
* "foo" | ||||||
* ] | ||||||
* }, | ||||||
* "keyword": { | ||||||
* "type": "keyword", | ||||||
* "searchable": false, | ||||||
* "aggregatable": true, | ||||||
* "non_aggregatable_indices": [ | ||||||
* "bar", "baz" | ||||||
* ] | ||||||
* } | ||||||
* } | ||||||
*/ | ||||||
private static Map<String, InvalidMappedField> getInvalidFieldsForAliases(String fieldName, Map<String, FieldCapabilities> types, | ||||||
ImmutableOpenMap<String, List<AliasMetaData>> aliases) { | ||||||
if (aliases == null || aliases.isEmpty()) { | ||||||
return emptyMap(); | ||||||
} | ||||||
Map<String, InvalidMappedField> invalidFields = new HashMap<>(); | ||||||
Map<String, Set<String>> typesErrors = new HashMap<>(); | ||||||
Map<String, Set<String>> aliasToIndices = new HashMap<>(); // map with aliases and their list of indices | ||||||
|
||||||
Iterator<ObjectObjectCursor<String, List<AliasMetaData>>> iter = aliases.iterator(); | ||||||
while (iter.hasNext()) { | ||||||
ObjectObjectCursor<String, List<AliasMetaData>> index = iter.next(); | ||||||
for (AliasMetaData aliasMetaData : index.value) { | ||||||
String aliasName = aliasMetaData.alias(); | ||||||
typesErrors.putIfAbsent(aliasName, new HashSet<>()); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this creates a HashSet regardless of the key presence - better to do the check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, this was intended like this. See this line where the field type for this field belonging to this alias is added to the list of unique field types. And here the size of the set is checked to see if there is more than one field type across all indices belonging to this alias. If there is more than one field type, then the field is considered "invalid" for this alias. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't follow -
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am aware of the |
||||||
aliasToIndices.putIfAbsent(aliasName, new HashSet<String>()); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can it be simply
Suggested change
|
||||||
aliasToIndices.get(aliasName).add(index.key); | ||||||
} | ||||||
} | ||||||
|
||||||
// iterate over each type | ||||||
for (Entry<String, FieldCapabilities> type : types.entrySet()) { | ||||||
if (type.getKey() == UNMAPPED) { | ||||||
continue; | ||||||
} | ||||||
String[] indices = type.getValue().indices(); | ||||||
// if there is a list of indices where this field type is defined | ||||||
if (indices != null) { | ||||||
// Look at all these indices' aliases and add the type of the field to a list (Set) with unique elements. | ||||||
// A valid mapping for a field in an index alias should contain only one type. If it doesn't this means that field | ||||||
// is mapped as different types across the indices in this index alias. | ||||||
for (String index : indices) { | ||||||
List<AliasMetaData> indexAliases = aliases.get(index); | ||||||
if (indexAliases == null) { | ||||||
continue; | ||||||
} | ||||||
for (AliasMetaData aliasMetaData : indexAliases) { | ||||||
typesErrors.get(aliasMetaData.alias()).add(type.getKey()); | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
for (Entry<String, Set<String>> entry : typesErrors.entrySet()) { | ||||||
String aliasName = entry.getKey(); | ||||||
// if, for the same index alias, there are multiple field types for this fieldName ie the index alias has indices where the same | ||||||
// field name is of different types | ||||||
if (entry.getValue().size() > 1) { | ||||||
// consider the field as invalid, for the currently checked index alias | ||||||
// the error message doesn't actually matter | ||||||
invalidFields.put(aliasName, new InvalidMappedField(fieldName)); | ||||||
} else { | ||||||
// if the field type is the same across all this alias' indices, check the field's capabilities (searchable/aggregatable) | ||||||
for (Entry<String, FieldCapabilities> type : types.entrySet()) { | ||||||
if (type.getKey() == UNMAPPED) { | ||||||
continue; | ||||||
} | ||||||
FieldCapabilities f = type.getValue(); | ||||||
|
||||||
// the existence of a list of non_aggregatable_indices is an indication that not all indices have the same capabilities | ||||||
// but this list can contain indices belonging to other aliases, so we need to check only for this alias | ||||||
if (f.nonAggregatableIndices() != null) { | ||||||
Set<String> aliasIndices = aliasToIndices.get(aliasName); | ||||||
int nonAggregatableCount = 0; | ||||||
// either all or none of the non-aggregatable indices belonging to a certain alias should be in this list | ||||||
for (String nonAggIndex : f.nonAggregatableIndices()) { | ||||||
if (aliasIndices.contains(nonAggIndex)) { | ||||||
nonAggregatableCount++; | ||||||
} | ||||||
} | ||||||
if (nonAggregatableCount > 0 && nonAggregatableCount != aliasIndices.size()) { | ||||||
invalidFields.put(aliasName, new InvalidMappedField(fieldName)); | ||||||
break; | ||||||
} | ||||||
} | ||||||
|
||||||
// perform the same check for non_searchable_indices list | ||||||
if (f.nonSearchableIndices() != null) { | ||||||
Set<String> aliasIndices = aliasToIndices.get(aliasName); | ||||||
int nonSearchableCount = 0; | ||||||
// either all or none of the non-searchable indices belonging to a certain alias should be in this list | ||||||
for (String nonSearchIndex : f.nonSearchableIndices()) { | ||||||
if (aliasIndices.contains(nonSearchIndex)) { | ||||||
nonSearchableCount++; | ||||||
} | ||||||
} | ||||||
if (nonSearchableCount > 0 && nonSearchableCount != aliasIndices.size()) { | ||||||
invalidFields.put(aliasName, new InvalidMappedField(fieldName)); | ||||||
break; | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
if (invalidFields.size() > 0) { | ||||||
return invalidFields; | ||||||
} | ||||||
// everything checks | ||||||
return emptyMap(); | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.sql.qa.single_node; | ||
|
||
import org.elasticsearch.xpack.sql.qa.jdbc.SysColumnsTestCase; | ||
|
||
public class SysColumnsIT extends SysColumnsTestCase { | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use static imports on
ActionListener
to make the code a bit more readable.