Skip to content

Commit 5832ff0

Browse files
Tiny refactoring around how wildcard is processed and added tests
1 parent 2653377 commit 5832ff0

File tree

2 files changed

+99
-24
lines changed

2 files changed

+99
-24
lines changed

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/parser/IdentifierBuilder.java

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import org.elasticsearch.core.Tuple;
1616
import org.elasticsearch.indices.InvalidIndexNameException;
1717
import org.elasticsearch.transport.RemoteClusterService;
18-
import org.elasticsearch.xpack.esql.core.util.Holder;
1918
import org.elasticsearch.xpack.esql.parser.EsqlBaseParser.IdentifierContext;
2019
import org.elasticsearch.xpack.esql.parser.EsqlBaseParser.IndexStringContext;
2120

@@ -89,19 +88,18 @@ public String visitSelectorString(EsqlBaseParser.SelectorStringContext ctx) {
8988

9089
public String visitIndexPattern(List<EsqlBaseParser.IndexPatternContext> ctx) {
9190
List<String> patterns = new ArrayList<>(ctx.size());
92-
Holder<Boolean> hasSeenStar = new Holder<>(false);
9391
ctx.forEach(c -> {
9492
String indexPattern = visitIndexString(c.indexString());
9593
String clusterString = visitClusterString(c.clusterString());
9694
String selectorString = visitSelectorString(c.selectorString());
9795

98-
hasSeenStar.set(indexPattern.contains(WILDCARD) || hasSeenStar.get());
99-
validateClusterAndIndexPatterns(indexPattern, c, hasSeenStar.get(), clusterString, selectorString);
96+
validateClusterAndIndexPatterns(indexPattern, c, clusterString, selectorString);
10097
patterns.add(reassembleIndexName(clusterString, indexPattern, selectorString));
10198
});
10299
return Strings.collectionToDelimitedString(patterns, ",");
103100
}
104101

102+
105103
private static void throwOnMixingSelectorWithCluster(String indexPattern, EsqlBaseParser.IndexPatternContext c) {
106104
InvalidIndexNameException ie = new InvalidIndexNameException(
107105
indexPattern,
@@ -134,7 +132,6 @@ protected static void validateClusterString(String clusterString, EsqlBaseParser
134132
private static void validateClusterAndIndexPatterns(
135133
String indexPattern,
136134
EsqlBaseParser.IndexPatternContext ctx,
137-
boolean hasSeenStar,
138135
String clusterString,
139136
String selectorString
140137
) {
@@ -193,7 +190,7 @@ private static void validateClusterAndIndexPatterns(
193190
validateClusterString(clusterString, ctx);
194191
}
195192

196-
validateIndicesForCluster(clusterString, indices, ctx, hasSeenStar);
193+
validateIndicesForCluster(clusterString, indices, ctx);
197194
if (selectorString != null) {
198195
try {
199196
// Ensures that the selector provided is one of the valid kinds
@@ -207,13 +204,7 @@ private static void validateClusterAndIndexPatterns(
207204
}
208205
}
209206

210-
private static void validateIndicesForCluster(
211-
String clusterString,
212-
String[] indices,
213-
EsqlBaseParser.IndexPatternContext ctx,
214-
boolean hasSeenStar
215-
) {
216-
boolean hasExclusion = false;
207+
private static void validateIndicesForCluster(String clusterString, String[] indices, EsqlBaseParser.IndexPatternContext ctx) {
217208
for (String index : indices) {
218209
// Strip spaces off first because validation checks are not written to handle them
219210
index = index.strip();
@@ -229,12 +220,12 @@ private static void validateIndicesForCluster(
229220
// throws exception if the selector expression is invalid. Selector resolution does not complain about exclusions
230221
throw new ParsingException(e, source(ctx), e.getMessage());
231222
}
232-
hasSeenStar = index.contains(WILDCARD) || hasSeenStar;
223+
var hasSeenStar = index.contains(WILDCARD);
233224
index = index.replace(WILDCARD, "").strip();
234225
if (index.isBlank()) {
235226
continue;
236227
}
237-
hasExclusion = index.startsWith(EXCLUSION);
228+
var hasExclusion = index.startsWith(EXCLUSION);
238229
index = removeExclusion(index);
239230
String tempName;
240231
try {

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/StatementParserTests.java

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
import static org.elasticsearch.xpack.esql.IdentifierGenerator.Features.DATE_MATH;
8686
import static org.elasticsearch.xpack.esql.IdentifierGenerator.Features.INDEX_SELECTOR;
8787
import static org.elasticsearch.xpack.esql.IdentifierGenerator.Features.WILDCARD_PATTERN;
88+
import static org.elasticsearch.xpack.esql.IdentifierGenerator.quote;
8889
import static org.elasticsearch.xpack.esql.IdentifierGenerator.randomIndexPattern;
8990
import static org.elasticsearch.xpack.esql.IdentifierGenerator.randomIndexPatterns;
9091
import static org.elasticsearch.xpack.esql.IdentifierGenerator.unquoteIndexPattern;
@@ -693,20 +694,19 @@ public void testInvalidCharacterInIndexPattern() {
693694
}
694695
lineNumber = command.contains("FROM") ? "line 1:9: " : "line 1:12: ";
695696
String indexStarLineNumber = command.contains("FROM") ? "line 1:14: " : "line 1:17: ";
696-
clustersAndIndices(command, "*", "-index#pattern");
697-
clustersAndIndices(command, "index*", "-index#pattern");
698-
clustersAndIndices(command, "*", "-<--logstash-{now/M{yyyy.MM}}>");
699-
clustersAndIndices(command, "index*", "-<--logstash#-{now/M{yyyy.MM}}>");
697+
expectInvalidIndexNameErrorWithLineNumber(command, "*, index#pattern", lineNumber, "index#pattern", "must not contain '#'");
698+
expectInvalidIndexNameErrorWithLineNumber(
699+
command,
700+
"index*, index#pattern",
701+
indexStarLineNumber,
702+
"index#pattern",
703+
"must not contain '#'"
704+
);
700705
expectDateMathErrorWithLineNumber(command, "*, \"-<-logstash-{now/D}>\"", lineNumber, dateMathError);
701706
expectDateMathErrorWithLineNumber(command, "*, -<-logstash-{now/D}>", lineNumber, dateMathError);
702707
expectDateMathErrorWithLineNumber(command, "\"*, -<-logstash-{now/D}>\"", commands.get(command), dateMathError);
703708
expectDateMathErrorWithLineNumber(command, "\"*, -<-logst:ash-{now/D}>\"", commands.get(command), dateMathError);
704709
if (EsqlCapabilities.Cap.INDEX_COMPONENT_SELECTORS.isEnabled()) {
705-
clustersAndIndices(command, "*", "-index#pattern::data");
706-
clustersAndIndices(command, "*", "-index#pattern::data");
707-
clustersAndIndices(command, "index*", "-index#pattern::data");
708-
clustersAndIndices(command, "*", "-<--logstash-{now/M{yyyy.MM}}>::data");
709-
clustersAndIndices(command, "index*", "-<--logstash#-{now/M{yyyy.MM}}>::data");
710710
// Throw on invalid date math
711711
expectDateMathErrorWithLineNumber(command, "*, \"-<-logstash-{now/D}>\"::data", lineNumber, dateMathError);
712712
expectDateMathErrorWithLineNumber(command, "*, -<-logstash-{now/D}>::data", lineNumber, dateMathError);
@@ -3108,6 +3108,90 @@ public void testValidJoinPattern() {
31083108
assertThat(joinType.coreJoin().joinName(), equalTo("LEFT OUTER"));
31093109
}
31103110

3111+
public void testInvalidPatternsWithIntermittentQuotes() {
3112+
// There are 3 ways of crafting an invalid index pattern that conforms to the grammar defined through ANTLR.
3113+
// 1. Quoting the entire pattern.
3114+
// 2. Quoting the cluster alias - "invalid cluster alias":<rest of the pattern>
3115+
// 3. Quoting the index name - <cluster alias>:"invalid index"
3116+
3117+
// Prohibited char in a quoted cross cluster index pattern should result in an error.
3118+
{
3119+
var firstValidIndexPattern = randomIndexPattern();
3120+
// Exclude date math expressions or its parsing might result in an error when an invalid char is sneaked in.
3121+
var secondRandomIndex = unquoteIndexPattern(randomIndexPattern(without(DATE_MATH), without(INDEX_SELECTOR)));
3122+
3123+
// Select an invalid char to sneak in.
3124+
char[] invalidChars = { ' ', '\"', '*', ',', '/', '<', '>', '?', '|' };
3125+
var randomInvalidChar = invalidChars[randomIntBetween(0, invalidChars.length - 1)];
3126+
3127+
// Find a random position to insert the invalid char.
3128+
var randomPos = randomIntBetween(3, secondRandomIndex.length() - 1);
3129+
// Construct the new invalid index pattern.
3130+
var remoteIndexWithInvalidChar = quote(
3131+
secondRandomIndex.substring(0, randomPos) + randomInvalidChar + secondRandomIndex.substring(randomPos + 1)
3132+
);
3133+
3134+
var query = "FROM " + firstValidIndexPattern + "," + remoteIndexWithInvalidChar;
3135+
expectError(query, "must not contain the following characters [' ','\"','*',',','/','<','>','?','\\','|']");
3136+
}
3137+
3138+
// Cluster names with invalid characters, i.e. ':' should result in an error.
3139+
{
3140+
var randomIndex = randomIndexPattern();
3141+
3142+
// In the form of: "*|cluster alias:random string".
3143+
var malformedClusterAlias = quote((randomBoolean() ? "*" : randomIdentifier()) + ":" + randomIdentifier());
3144+
3145+
// We do not generate a cross cluster pattern or else we'd be getting a different error (which is tested in
3146+
// the next test).
3147+
var remoteIndex = randomIndexPattern(without(CROSS_CLUSTER));
3148+
// Format: FROM <some index>, "<cluster alias: random string>":<remote index>
3149+
var query = "FROM " + randomIndex + "," + malformedClusterAlias + ":" + remoteIndex;
3150+
expectError(query, "cluster string [" + unquoteIndexPattern(malformedClusterAlias) + "] must not contain ':'");
3151+
}
3152+
3153+
// If a remote index is quoted and has a remote cluster alias and the pattern is already prefixed with
3154+
// a cluster alias, we should flag the cluster alias in the pattern.
3155+
{
3156+
var randomIndex = randomIndexPattern();
3157+
var remoteClusterAlias = randomBoolean() ? "*" : randomIdentifier();
3158+
// In the form of: random string:random string.
3159+
var malformedRemoteIndex = quote(unquoteIndexPattern(randomIndexPattern(CROSS_CLUSTER)));
3160+
// Format: FROM <some index>, <cluster alias>:"random string:random string"
3161+
var query = "FROM " + randomIndex + "," + remoteClusterAlias + ":" + malformedRemoteIndex;
3162+
expectError(query, "contains a cluster alias despite specifying one");
3163+
}
3164+
3165+
if (EsqlCapabilities.Cap.INDEX_COMPONENT_SELECTORS.isEnabled()) {
3166+
// If a stream in on a remote and the pattern is entirely quoted, we should be able to validate it.
3167+
// Note: invalid selector syntax is covered in a different test.
3168+
{
3169+
var fromPattern = randomIndexPattern();
3170+
var malformedIndexSelectorPattern = quote(
3171+
(randomIdentifier()) + ":" + unquoteIndexPattern(randomIndexPattern(INDEX_SELECTOR, without(CROSS_CLUSTER)))
3172+
);
3173+
// Format: FROM <some index>, "<cluster alias>:<some index>::<data|failures>"
3174+
var query = "FROM " + fromPattern + "," + malformedIndexSelectorPattern;
3175+
expectError(query, "Selectors are not yet supported on remote cluster patterns");
3176+
}
3177+
3178+
// If a stream in on a remote and the cluster alias and index pattern are separately quoted, we should
3179+
// still be able to validate it.
3180+
// Note:
3181+
// 1. Invalid selector syntax is covered in a different test.
3182+
// 2. We unquote a pattern and re-quote it to prevent partial quoting of pattern fragments.
3183+
{
3184+
var fromPattern = randomIndexPattern();
3185+
var malformedIndexSelectorPattern = quote(randomIdentifier())
3186+
+ ":"
3187+
+ quote(unquoteIndexPattern(randomIndexPattern(INDEX_SELECTOR, without(CROSS_CLUSTER))));
3188+
// Format: FROM <some index>, "<cluster alias>":"<some index>::<data|failures>"
3189+
var query = "FROM " + fromPattern + "," + malformedIndexSelectorPattern;
3190+
expectError(query, "Selectors are not yet supported on remote cluster patterns");
3191+
}
3192+
}
3193+
}
3194+
31113195
public void testInvalidJoinPatterns() {
31123196
assumeTrue("LOOKUP JOIN requires corresponding capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled());
31133197

0 commit comments

Comments
 (0)