Skip to content

Commit ca6808e

Browse files
authored
SQL: Support pattern against compatible indices (#34718)
Extend querying support on multiple indices from being strictly identical to being just compatible. Use FieldCapabilities API (extended through #33803) for mapping merging. Close #31837 #31611
1 parent 36baf38 commit ca6808e

File tree

25 files changed

+727
-348
lines changed

25 files changed

+727
-348
lines changed

docs/reference/sql/security.asciidoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ indices:
3434

3535
["source","yaml",subs="attributes,callouts,macros"]
3636
--------------------------------------------------
37-
include-tagged::{sql-tests}/security/roles.yml[cli_jdbc]
37+
include-tagged::{sql-tests}/security/roles.yml[cli_drivers]
3838
--------------------------------------------------
3939

x-pack/plugin/sql/sql-proto/src/main/java/org/elasticsearch/xpack/sql/type/DataType.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ public enum DataType {
3636
SCALED_FLOAT(JDBCType.FLOAT, Double.class, Double.BYTES, 19, 25, false, true, true),
3737
KEYWORD( JDBCType.VARCHAR, String.class, Integer.MAX_VALUE, 256, 0),
3838
TEXT( JDBCType.VARCHAR, String.class, Integer.MAX_VALUE, Integer.MAX_VALUE, 0, false, false, false),
39-
OBJECT( JDBCType.STRUCT, null, -1, 0, 0),
40-
NESTED( JDBCType.STRUCT, null, -1, 0, 0),
39+
OBJECT( JDBCType.STRUCT, null, -1, 0, 0, false, false, false),
40+
NESTED( JDBCType.STRUCT, null, -1, 0, 0, false, false, false),
4141
BINARY( JDBCType.VARBINARY, byte[].class, -1, Integer.MAX_VALUE, 0),
4242
// since ODBC and JDBC interpret precision for Date as display size,
4343
// the precision is 23 (number of chars in ISO8601 with millis) + Z (the UTC timezone)
@@ -223,7 +223,11 @@ public static DataType fromODBCType(String odbcType) {
223223
* For any dataType DataType.fromEsType(dataType.esType) == dataType
224224
*/
225225
public static DataType fromEsType(String esType) {
226-
return DataType.valueOf(esType.toUpperCase(Locale.ROOT));
226+
try {
227+
return DataType.valueOf(esType.toUpperCase(Locale.ROOT));
228+
} catch (IllegalArgumentException ex) {
229+
return DataType.UNSUPPORTED;
230+
}
227231
}
228232

229233
public boolean isCompatibleWith(DataType other) {

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

Lines changed: 151 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
1616
import org.elasticsearch.action.admin.indices.get.GetIndexRequest.Feature;
1717
import org.elasticsearch.action.admin.indices.get.GetIndexResponse;
18+
import org.elasticsearch.action.fieldcaps.FieldCapabilities;
19+
import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest;
1820
import org.elasticsearch.action.support.IndicesOptions;
1921
import org.elasticsearch.action.support.IndicesOptions.Option;
2022
import org.elasticsearch.action.support.IndicesOptions.WildcardStates;
@@ -24,23 +26,34 @@
2426
import org.elasticsearch.common.Strings;
2527
import org.elasticsearch.common.collect.ImmutableOpenMap;
2628
import org.elasticsearch.index.IndexNotFoundException;
29+
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
30+
import org.elasticsearch.xpack.sql.type.DataType;
31+
import org.elasticsearch.xpack.sql.type.DateEsField;
2732
import org.elasticsearch.xpack.sql.type.EsField;
33+
import org.elasticsearch.xpack.sql.type.KeywordEsField;
34+
import org.elasticsearch.xpack.sql.type.TextEsField;
2835
import org.elasticsearch.xpack.sql.type.Types;
36+
import org.elasticsearch.xpack.sql.type.UnsupportedEsField;
2937
import org.elasticsearch.xpack.sql.util.CollectionUtils;
3038

3139
import java.util.ArrayList;
40+
import java.util.Arrays;
3241
import java.util.Collections;
3342
import java.util.Comparator;
3443
import java.util.EnumSet;
44+
import java.util.LinkedHashMap;
3545
import java.util.List;
3646
import java.util.Locale;
3747
import java.util.Map;
48+
import java.util.Map.Entry;
49+
import java.util.NavigableSet;
3850
import java.util.Objects;
3951
import java.util.Set;
52+
import java.util.TreeMap;
4053
import java.util.TreeSet;
4154
import java.util.regex.Pattern;
4255

43-
import static java.util.Collections.emptyList;
56+
import static java.util.Collections.emptyMap;
4457

4558
public class IndexResolver {
4659

@@ -222,64 +235,157 @@ private void filterResults(String javaRegex, GetAliasesResponse aliases, GetInde
222235
listener.onResponse(result);
223236
}
224237

225-
226238
/**
227239
* Resolves a pattern to one (potentially compound meaning that spawns multiple indices) mapping.
228240
*/
229-
public void resolveWithSameMapping(String indexWildcard, String javaRegex, ActionListener<IndexResolution> listener) {
230-
GetIndexRequest getIndexRequest = createGetIndexRequest(indexWildcard);
231-
client.admin().indices().getIndex(getIndexRequest, ActionListener.wrap(response -> {
232-
ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings = response.getMappings();
233-
234-
List<IndexResolution> resolutions;
235-
if (mappings.size() > 0) {
236-
resolutions = new ArrayList<>(mappings.size());
237-
Pattern pattern = javaRegex != null ? Pattern.compile(javaRegex) : null;
238-
for (ObjectObjectCursor<String, ImmutableOpenMap<String, MappingMetaData>> indexMappings : mappings) {
239-
String concreteIndex = indexMappings.key;
240-
if (pattern == null || pattern.matcher(concreteIndex).matches()) {
241-
resolutions.add(buildGetIndexResult(concreteIndex, concreteIndex, indexMappings.value));
241+
public void resolveAsMergedMapping(String indexWildcard, String javaRegex, ActionListener<IndexResolution> listener) {
242+
FieldCapabilitiesRequest fieldRequest = createFieldCapsRequest(indexWildcard);
243+
client.fieldCaps(fieldRequest,
244+
ActionListener.wrap(response -> listener.onResponse(mergedMapping(indexWildcard, response.get())), listener::onFailure));
245+
}
246+
247+
static IndexResolution mergedMapping(String indexPattern, Map<String, Map<String, FieldCapabilities>> fieldCaps) {
248+
if (fieldCaps == null || fieldCaps.isEmpty()) {
249+
return IndexResolution.notFound(indexPattern);
250+
}
251+
252+
StringBuilder errorMessage = new StringBuilder();
253+
254+
NavigableSet<Entry<String, Map<String, FieldCapabilities>>> sortedFields = new TreeSet<>(
255+
// for some reason .reversed doesn't work (prolly due to inference)
256+
Collections.reverseOrder(Comparator.comparing(Entry::getKey)));
257+
sortedFields.addAll(fieldCaps.entrySet());
258+
259+
Map<String, EsField> hierarchicalMapping = new TreeMap<>();
260+
Map<String, EsField> flattedMapping = new LinkedHashMap<>();
261+
262+
// sort keys descending in order to easily detect multi-fields (a.b.c multi-field of a.b)
263+
// without sorting, they can still be detected however without the emptyMap optimization
264+
// (fields without multi-fields have no children)
265+
for (Entry<String, Map<String, FieldCapabilities>> entry : sortedFields) {
266+
String name = entry.getKey();
267+
// skip internal fields
268+
if (!name.startsWith("_")) {
269+
Map<String, FieldCapabilities> types = entry.getValue();
270+
// field is mapped differently across indices
271+
if (types.size() > 1) {
272+
// build error message
273+
for (Entry<String, FieldCapabilities> type : types.entrySet()) {
274+
if (errorMessage.length() > 0) {
275+
errorMessage.append(", ");
276+
}
277+
errorMessage.append("[");
278+
errorMessage.append(type.getKey());
279+
errorMessage.append("] in ");
280+
errorMessage.append(Arrays.toString(type.getValue().indices()));
242281
}
282+
283+
errorMessage.insert(0,
284+
"[" + indexPattern + "] points to indices with incompatible mappings; " +
285+
"field [" + name + "] is mapped in [" + types.size() + "] different ways: ");
286+
}
287+
if (errorMessage.length() > 0) {
288+
return IndexResolution.invalid(errorMessage.toString());
289+
}
290+
291+
FieldCapabilities fieldCap = types.values().iterator().next();
292+
// validate search/agg-able
293+
if (fieldCap.isAggregatable() && fieldCap.nonAggregatableIndices() != null) {
294+
errorMessage.append("[" + indexPattern + "] points to indices with incompatible mappings: ");
295+
errorMessage.append("field [" + name + "] is aggregateable except in ");
296+
errorMessage.append(Arrays.toString(fieldCap.nonAggregatableIndices()));
297+
}
298+
if (fieldCap.isSearchable() && fieldCap.nonSearchableIndices() != null) {
299+
if (errorMessage.length() > 0) {
300+
errorMessage.append(",");
301+
}
302+
errorMessage.append("[" + indexPattern + "] points to indices with incompatible mappings: ");
303+
errorMessage.append("field [" + name + "] is searchable except in ");
304+
errorMessage.append(Arrays.toString(fieldCap.nonSearchableIndices()));
305+
}
306+
if (errorMessage.length() > 0) {
307+
return IndexResolution.invalid(errorMessage.toString());
308+
}
309+
310+
// validation passes - create the field
311+
// and name wasn't added before
312+
if (!flattedMapping.containsKey(name)) {
313+
createField(name, fieldCap, fieldCaps, hierarchicalMapping, flattedMapping, false);
243314
}
244-
} else {
245-
resolutions = emptyList();
246315
}
316+
}
247317

248-
listener.onResponse(merge(resolutions, indexWildcard));
249-
}, listener::onFailure));
318+
return IndexResolution.valid(new EsIndex(indexPattern, hierarchicalMapping));
250319
}
251320

252-
static IndexResolution merge(List<IndexResolution> resolutions, String indexWildcard) {
253-
IndexResolution merged = null;
254-
for (IndexResolution resolution : resolutions) {
255-
// everything that follows gets compared
256-
if (!resolution.isValid()) {
257-
return resolution;
258-
}
259-
// initialize resolution on first run
260-
if (merged == null) {
261-
merged = resolution;
262-
}
263-
// need the same mapping across all resolutions
264-
if (!merged.get().mapping().equals(resolution.get().mapping())) {
265-
return IndexResolution.invalid(
266-
"[" + indexWildcard + "] points to indices [" + merged.get().name() + "] "
267-
+ "and [" + resolution.get().name() + "] which have different mappings. "
268-
+ "When using multiple indices, the mappings must be identical.");
321+
private static EsField createField(String fieldName, FieldCapabilities caps, Map<String, Map<String, FieldCapabilities>> globalCaps,
322+
Map<String, EsField> hierarchicalMapping, Map<String, EsField> flattedMapping, boolean hasChildren) {
323+
324+
Map<String, EsField> parentProps = hierarchicalMapping;
325+
326+
int dot = fieldName.lastIndexOf('.');
327+
String fullFieldName = fieldName;
328+
329+
if (dot >= 0) {
330+
String parentName = fieldName.substring(0, dot);
331+
fieldName = fieldName.substring(dot + 1);
332+
EsField parent = flattedMapping.get(parentName);
333+
if (parent == null) {
334+
Map<String, FieldCapabilities> map = globalCaps.get(parentName);
335+
if (map == null) {
336+
throw new SqlIllegalArgumentException("Cannot find field {}; this is likely a bug", parentName);
337+
}
338+
FieldCapabilities parentCap = map.values().iterator().next();
339+
parent = createField(parentName, parentCap, globalCaps, hierarchicalMapping, flattedMapping, true);
269340
}
341+
parentProps = parent.getProperties();
270342
}
271-
if (merged != null) {
272-
// at this point, we are sure there's the same mapping across all (if that's the case) indices
273-
// to keep things simple, use the given pattern as index name
274-
merged = IndexResolution.valid(new EsIndex(indexWildcard, merged.get().mapping()));
275-
} else {
276-
merged = IndexResolution.notFound(indexWildcard);
343+
344+
EsField field = null;
345+
Map<String, EsField> props = hasChildren ? new TreeMap<>() : emptyMap();
346+
347+
DataType esType = DataType.fromEsType(caps.getType());
348+
switch (esType) {
349+
case TEXT:
350+
field = new TextEsField(fieldName, props, false);
351+
break;
352+
case KEYWORD:
353+
int length = DataType.KEYWORD.defaultPrecision;
354+
// TODO: to check whether isSearchable/isAggregateable takes into account the presence of the normalizer
355+
boolean normalized = false;
356+
field = new KeywordEsField(fieldName, props, caps.isAggregatable(), length, normalized);
357+
break;
358+
case DATE:
359+
field = new DateEsField(fieldName, props, caps.isAggregatable());
360+
break;
361+
case UNSUPPORTED:
362+
field = new UnsupportedEsField(fieldName, caps.getType());
363+
break;
364+
default:
365+
field = new EsField(fieldName, esType, props, caps.isAggregatable());
277366
}
278-
return merged;
367+
368+
parentProps.put(fieldName, field);
369+
flattedMapping.put(fullFieldName, field);
370+
371+
return field;
372+
}
373+
374+
private static FieldCapabilitiesRequest createFieldCapsRequest(String index) {
375+
return new FieldCapabilitiesRequest()
376+
.indices(Strings.commaDelimitedListToStringArray(index))
377+
.fields("*")
378+
//lenient because we throw our own errors looking at the response e.g. if something was not resolved
379+
//also because this way security doesn't throw authorization exceptions but rather honors ignore_unavailable
380+
.indicesOptions(IndicesOptions.lenientExpandOpen());
279381
}
280382

383+
// TODO: Concrete indices still uses get mapping
384+
// waiting on https://github.com/elastic/elasticsearch/pull/34071
385+
//
386+
281387
/**
282-
* Resolves a pattern to multiple, separate indices.
388+
* Resolves a pattern to multiple, separate indices. Doesn't perform validation.
283389
*/
284390
public void resolveAsSeparateMappings(String indexWildcard, String javaRegex, ActionListener<List<EsIndex>> listener) {
285391
GetIndexRequest getIndexRequest = createGetIndexRequest(indexWildcard);
@@ -306,7 +412,7 @@ public void resolveAsSeparateMappings(String indexWildcard, String javaRegex, Ac
306412
listener.onResponse(results);
307413
}, listener::onFailure));
308414
}
309-
415+
310416
private static GetIndexRequest createGetIndexRequest(String index) {
311417
return new GetIndexRequest()
312418
.local(true)

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/plan/logical/command/ShowColumns.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@ protected NodeInfo<ShowColumns> info() {
5454
@Override
5555
public List<Attribute> output() {
5656
return asList(new FieldAttribute(location(), "column", new KeywordEsField("column")),
57-
new FieldAttribute(location(), "type", new KeywordEsField("type"))); }
57+
new FieldAttribute(location(), "type", new KeywordEsField("type")),
58+
new FieldAttribute(location(), "mapping", new KeywordEsField("mapping")));
59+
}
5860

5961
@Override
6062
public void execute(SqlSession session, ActionListener<SchemaRowSet> listener) {
6163
String idx = index != null ? index : (pattern != null ? pattern.asIndexNameWildcard() : "*");
6264
String regex = pattern != null ? pattern.asJavaRegex() : null;
63-
session.indexResolver().resolveWithSameMapping(idx, regex, ActionListener.wrap(
65+
session.indexResolver().resolveAsMergedMapping(idx, regex, ActionListener.wrap(
6466
indexResult -> {
6567
List<List<?>> rows = emptyList();
6668
if (indexResult.isValid()) {
@@ -69,8 +71,7 @@ public void execute(SqlSession session, ActionListener<SchemaRowSet> listener) {
6971
}
7072
listener.onResponse(Rows.of(output(), rows));
7173
},
72-
listener::onFailure
73-
));
74+
listener::onFailure));
7475
}
7576

7677
private void fillInRows(Map<String, EsField> mapping, String prefix, List<List<?>> rows) {
@@ -79,7 +80,7 @@ private void fillInRows(Map<String, EsField> mapping, String prefix, List<List<?
7980
DataType dt = field.getDataType();
8081
String name = e.getKey();
8182
if (dt != null) {
82-
rows.add(asList(prefix != null ? prefix + "." + name : name, dt.sqlName()));
83+
rows.add(asList(prefix != null ? prefix + "." + name : name, dt.sqlName(), dt.name()));
8384
if (field.getProperties().isEmpty() == false) {
8485
String newPrefix = prefix != null ? prefix + "." + name : name;
8586
fillInRows(field.getProperties(), newPrefix, rows);

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ private String aliasName(Attribute attr) {
172172
// reference methods
173173
//
174174
private FieldExtraction topHitFieldRef(FieldAttribute fieldAttr) {
175-
return new SearchHitFieldRef(aliasName(fieldAttr), fieldAttr.field().getDataType(), fieldAttr.field().hasDocValues());
175+
return new SearchHitFieldRef(aliasName(fieldAttr), fieldAttr.field().getDataType(), fieldAttr.field().isAggregatable());
176176
}
177177

178178
private Tuple<QueryContainer, FieldExtraction> nestedHitFieldRef(FieldAttribute attr) {
@@ -181,10 +181,10 @@ private Tuple<QueryContainer, FieldExtraction> nestedHitFieldRef(FieldAttribute
181181

182182
String name = aliasName(attr);
183183
Query q = rewriteToContainNestedField(query, attr.location(),
184-
attr.nestedParent().name(), name, attr.field().hasDocValues());
184+
attr.nestedParent().name(), name, attr.field().isAggregatable());
185185

186186
SearchHitFieldRef nestedFieldRef = new SearchHitFieldRef(name, attr.field().getDataType(),
187-
attr.field().hasDocValues(), attr.parent().name());
187+
attr.field().isAggregatable(), attr.parent().name());
188188
nestedRefs.add(nestedFieldRef);
189189

190190
return new Tuple<>(new QueryContainer(q, aggs, columns, aliases, pseudoFunctions, scalarFunctions, sort, limit), nestedFieldRef);

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/session/SqlSession.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ private <T> void preAnalyze(LogicalPlan parsed, Function<IndexResolution, T> act
127127
listener.onFailure(new MappingException("Cannot inspect indices in cluster/catalog [{}]", cluster));
128128
}
129129

130-
indexResolver.resolveWithSameMapping(table.index(), null,
130+
indexResolver.resolveAsMergedMapping(table.index(), null,
131131
wrap(indexResult -> listener.onResponse(action.apply(indexResult)), listener::onFailure));
132132
} else {
133133
try {

0 commit comments

Comments
 (0)