Skip to content
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

Merged
merged 11 commits into from
Mar 17, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -460,15 +463,31 @@ public void resolveAsSeparateMappings(String indexWildcard, String javaRegex, bo
FieldCapabilitiesRequest fieldRequest = createFieldCapsRequest(indexWildcard, includeFrozen);
client.fieldCaps(fieldRequest,
ActionListener.wrap(
Copy link
Member

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.

response -> listener.onResponse(
separateMappings(typeRegistry, indexWildcard, javaRegex, response.getIndices(), response.get())),
response -> {
Copy link
Member

Choose a reason for hiding this comment

The 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:

client.fieldCaps(fieldRequest, wrap(response -> getIndexAliases(response, wrap(aliases -> ...))

String[] indicesList = response.getIndices();
GetAliasesRequest aliasRequest = new GetAliasesRequest()
.local(true)
.aliases("*")
.indices(indicesList)
.indicesOptions(IndicesOptions.lenientExpandOpen());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IndicesOption depends on whether includeFrozen is true or not.

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 {
Expand All @@ -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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use org.elasticsearch.common.util.CollectionUtils.isEmpty instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not possible unfortunately, because aliases is an ImmutableOpenMap, which is not a Collection.

return emptyList();
}

final List<String> resolvedIndices = asList(indexNames);
Map<String, Fields> indices = new LinkedHashMap<>(resolvedIndices.size());
Set<String> resolvedAliases = new LinkedHashSet<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it okay to put all aliases in the same map?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for(AliasMetaData alias : iterator.next().value) {
for (AliasMetaData alias : iterator.next().value) {

resolvedAliases.add(alias.getAlias());
}
}
}

List<String> resolvedIndices = new ArrayList<>(asList(indexNames));
Map<String, Fields> indices = new LinkedHashMap<>(resolvedIndices.size() + resolvedAliases.size());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The size is incorrect - use CollectionUtils.mapSize

Pattern pattern = javaRegex != null ? Pattern.compile(javaRegex) : null;

// sort fields in reverse order to build the field hierarchy
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -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()));
}
}
}
Expand All @@ -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<>());
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow - putIfAbsent does if (key == null) put(key, value) - I'm pointing out that a new HashSet is created every time.
What I'm suggesting:

if (key == null) {
   put(key, new HashSet())
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am aware of the putIfAbsent functionality. I think I can simplify the logic a bit.

aliasToIndices.putIfAbsent(aliasName, new HashSet<String>());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be simply <>?

Suggested change
aliasToIndices.putIfAbsent(aliasName, new HashSet<String>());
aliasToIndices.putIfAbsent(aliasName, new HashSet<>());

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
Expand Up @@ -25,6 +25,11 @@ public InvalidMappedField(String name, String errorMessage) {
this.errorMessage = errorMessage;
}

public InvalidMappedField(String name) {
super(name, DataTypes.UNSUPPORTED, emptyMap(), false);
this.errorMessage = StringUtils.EMPTY;
}

public String errorMessage() {
return errorMessage;
}
Expand Down
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 {

}
Loading