mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-06-28 17:34:17 -04:00
ES|QL: Check if cluster aliases and index patterns are valid before executing query (#122497)
ES|QL index patterns validation: Ensure that the patterns in the query are syntactically and semantically valid --------- Co-authored-by: elasticsearchmachine <infra-root+elasticsearchmachine@elastic.co> Co-authored-by: Andrei Stefan <astefan@users.noreply.github.com> Co-authored-by: Alexander Spies <alexander.spies@elastic.co>
This commit is contained in:
parent
af735accda
commit
c94c021d0e
3 changed files with 408 additions and 82 deletions
5
docs/changelog/122497.yaml
Normal file
5
docs/changelog/122497.yaml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pr: 122497
|
||||||
|
summary: Check if index patterns conform to valid format before validation
|
||||||
|
area: CCS
|
||||||
|
type: enhancement
|
||||||
|
issues: []
|
|
@ -20,17 +20,20 @@ import org.elasticsearch.xpack.esql.parser.EsqlBaseParser.IdentifierContext;
|
||||||
import org.elasticsearch.xpack.esql.parser.EsqlBaseParser.IndexStringContext;
|
import org.elasticsearch.xpack.esql.parser.EsqlBaseParser.IndexStringContext;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.SelectorResolver.SELECTOR_SEPARATOR;
|
import static org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.SelectorResolver.SELECTOR_SEPARATOR;
|
||||||
import static org.elasticsearch.transport.RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR;
|
import static org.elasticsearch.transport.RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR;
|
||||||
import static org.elasticsearch.transport.RemoteClusterAware.isRemoteIndexName;
|
import static org.elasticsearch.transport.RemoteClusterAware.splitIndexName;
|
||||||
import static org.elasticsearch.xpack.esql.core.util.StringUtils.EXCLUSION;
|
import static org.elasticsearch.xpack.esql.core.util.StringUtils.EXCLUSION;
|
||||||
import static org.elasticsearch.xpack.esql.core.util.StringUtils.WILDCARD;
|
import static org.elasticsearch.xpack.esql.core.util.StringUtils.WILDCARD;
|
||||||
import static org.elasticsearch.xpack.esql.parser.ParserUtils.source;
|
import static org.elasticsearch.xpack.esql.parser.ParserUtils.source;
|
||||||
|
|
||||||
abstract class IdentifierBuilder extends AbstractBuilder {
|
abstract class IdentifierBuilder extends AbstractBuilder {
|
||||||
|
|
||||||
|
private static final String BLANK_INDEX_ERROR_MESSAGE = "Blank index specified in index pattern";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String visitIdentifier(IdentifierContext ctx) {
|
public String visitIdentifier(IdentifierContext ctx) {
|
||||||
return ctx == null ? null : unquoteIdentifier(ctx.QUOTED_IDENTIFIER(), ctx.UNQUOTED_IDENTIFIER());
|
return ctx == null ? null : unquoteIdentifier(ctx.QUOTED_IDENTIFIER(), ctx.UNQUOTED_IDENTIFIER());
|
||||||
|
@ -88,39 +91,21 @@ abstract class IdentifierBuilder extends AbstractBuilder {
|
||||||
String indexPattern = c.unquotedIndexString() != null ? c.unquotedIndexString().getText() : visitIndexString(c.indexString());
|
String indexPattern = c.unquotedIndexString() != null ? c.unquotedIndexString().getText() : visitIndexString(c.indexString());
|
||||||
String clusterString = visitClusterString(c.clusterString());
|
String clusterString = visitClusterString(c.clusterString());
|
||||||
String selectorString = visitSelectorString(c.selectorString());
|
String selectorString = visitSelectorString(c.selectorString());
|
||||||
// skip validating index on remote cluster, because the behavior of remote cluster is not consistent with local cluster
|
|
||||||
// For example, invalid#index is an invalid index name, however FROM *:invalid#index does not return an error
|
hasSeenStar.set(hasSeenStar.get() || indexPattern.contains(WILDCARD));
|
||||||
if (clusterString == null) {
|
validate(clusterString, indexPattern, selectorString, c, hasSeenStar.get());
|
||||||
hasSeenStar.set(indexPattern.contains(WILDCARD) || hasSeenStar.get());
|
|
||||||
validateIndexPattern(indexPattern, c, hasSeenStar.get());
|
|
||||||
// Other instances of Elasticsearch may have differing selectors so only validate selector string if remote cluster
|
|
||||||
// string is unset
|
|
||||||
if (selectorString != null) {
|
|
||||||
try {
|
|
||||||
// Ensures that the selector provided is one of the valid kinds
|
|
||||||
IndexNameExpressionResolver.SelectorResolver.validateIndexSelectorString(indexPattern, selectorString);
|
|
||||||
} catch (InvalidIndexNameException e) {
|
|
||||||
throw new ParsingException(e, source(c), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
validateClusterString(clusterString, c);
|
|
||||||
// Do not allow selectors on remote cluster expressions until they are supported
|
|
||||||
if (selectorString != null) {
|
|
||||||
throwOnMixingSelectorWithCluster(reassembleIndexName(clusterString, indexPattern, selectorString), c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
patterns.add(reassembleIndexName(clusterString, indexPattern, selectorString));
|
patterns.add(reassembleIndexName(clusterString, indexPattern, selectorString));
|
||||||
});
|
});
|
||||||
return Strings.collectionToDelimitedString(patterns, ",");
|
return Strings.collectionToDelimitedString(patterns, ",");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void throwInvalidIndexNameException(String indexPattern, String message, EsqlBaseParser.IndexPatternContext ctx) {
|
||||||
|
var ie = new InvalidIndexNameException(indexPattern, message);
|
||||||
|
throw new ParsingException(ie, source(ctx), ie.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
private static void throwOnMixingSelectorWithCluster(String indexPattern, EsqlBaseParser.IndexPatternContext c) {
|
private static void throwOnMixingSelectorWithCluster(String indexPattern, EsqlBaseParser.IndexPatternContext c) {
|
||||||
InvalidIndexNameException ie = new InvalidIndexNameException(
|
throwInvalidIndexNameException(indexPattern, "Selectors are not yet supported on remote cluster patterns", c);
|
||||||
indexPattern,
|
|
||||||
"Selectors are not yet supported on remote cluster patterns"
|
|
||||||
);
|
|
||||||
throw new ParsingException(ie, source(c), ie.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String reassembleIndexName(String clusterString, String indexPattern, String selectorString) {
|
private static String reassembleIndexName(String clusterString, String indexPattern, String selectorString) {
|
||||||
|
@ -144,56 +129,193 @@ abstract class IdentifierBuilder extends AbstractBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateIndexPattern(String indexPattern, EsqlBaseParser.IndexPatternContext ctx, boolean hasSeenStar) {
|
/**
|
||||||
// multiple index names can be in the same double quote, e.g. indexPattern = "idx1, *, -idx2"
|
* Takes the parsed constituent strings and validates them.
|
||||||
String[] indices = indexPattern.split(",");
|
* @param clusterString Name of the remote cluster. Can be null.
|
||||||
boolean hasExclusion = false;
|
* @param indexPattern Name of the index or pattern; can also have multiple patterns in case of quoting,
|
||||||
for (String index : indices) {
|
* e.g. {@code FROM """index*,-index1"""}.
|
||||||
// Strip spaces off first because validation checks are not written to handle them
|
* @param selectorString Selector string, i.e. "::data" or "::failures". Can be null.
|
||||||
index = index.strip();
|
* @param ctx Index Pattern Context for generating error messages with offsets.
|
||||||
if (isRemoteIndexName(index)) { // skip the validation if there is remote cluster
|
* @param hasSeenStar If we've seen an asterisk so far.
|
||||||
// Ensure that there are no selectors as they are not yet supported
|
*/
|
||||||
if (index.contains(SELECTOR_SEPARATOR)) {
|
private static void validate(
|
||||||
throwOnMixingSelectorWithCluster(index, ctx);
|
String clusterString,
|
||||||
}
|
String indexPattern,
|
||||||
continue;
|
String selectorString,
|
||||||
|
EsqlBaseParser.IndexPatternContext ctx,
|
||||||
|
boolean hasSeenStar
|
||||||
|
) {
|
||||||
|
/*
|
||||||
|
* At this point, only 3 formats are possible:
|
||||||
|
* "index_pattern(s)",
|
||||||
|
* remote:index_pattern, and,
|
||||||
|
* index_pattern::selector_string.
|
||||||
|
*
|
||||||
|
* The grammar prohibits remote:"index_pattern(s)" or "index_pattern(s)"::selector_string as they're
|
||||||
|
* partially quoted. So if either of cluster string or selector string are present, there's no need
|
||||||
|
* to split the pattern by comma since comma requires partial quoting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
String[] patterns;
|
||||||
|
if (clusterString == null && selectorString == null) {
|
||||||
|
// Pattern could be quoted or is singular like "index_name".
|
||||||
|
patterns = indexPattern.split(",", -1);
|
||||||
|
} else {
|
||||||
|
// Either of cluster string or selector string is present. Pattern is unquoted.
|
||||||
|
patterns = new String[] { indexPattern };
|
||||||
|
}
|
||||||
|
|
||||||
|
patterns = Arrays.stream(patterns).map(String::strip).toArray(String[]::new);
|
||||||
|
if (Arrays.stream(patterns).anyMatch(String::isBlank)) {
|
||||||
|
throwInvalidIndexNameException(indexPattern, BLANK_INDEX_ERROR_MESSAGE, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edge case: happens when all the index names in a pattern are empty like "FROM ",,,,,"".
|
||||||
|
if (patterns.length == 0) {
|
||||||
|
throwInvalidIndexNameException(indexPattern, BLANK_INDEX_ERROR_MESSAGE, ctx);
|
||||||
|
} else if (patterns.length == 1) {
|
||||||
|
// Pattern is either an unquoted string or a quoted string with a single index (no comma sep).
|
||||||
|
validateSingleIndexPattern(clusterString, patterns[0], selectorString, ctx, hasSeenStar);
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
* Presence of multiple patterns requires a comma and comma requires quoting. If quoting is present,
|
||||||
|
* cluster string and selector string cannot be present; they need to be attached within the quoting.
|
||||||
|
* So we attempt to extract them later.
|
||||||
|
*/
|
||||||
|
for (String pattern : patterns) {
|
||||||
|
validateSingleIndexPattern(null, pattern, null, ctx, hasSeenStar);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the constituent strings. Will extract the cluster string and/or selector string from the index
|
||||||
|
* name if clubbed together inside a quoted string.
|
||||||
|
*
|
||||||
|
* @param clusterString Name of the remote cluster. Can be null.
|
||||||
|
* @param indexName Name of the index.
|
||||||
|
* @param selectorString Selector string, i.e. "::data" or "::failures". Can be null.
|
||||||
|
* @param ctx Index Pattern Context for generating error messages with offsets.
|
||||||
|
* @param hasSeenStar If we've seen an asterisk so far.
|
||||||
|
*/
|
||||||
|
private static void validateSingleIndexPattern(
|
||||||
|
String clusterString,
|
||||||
|
String indexName,
|
||||||
|
String selectorString,
|
||||||
|
EsqlBaseParser.IndexPatternContext ctx,
|
||||||
|
boolean hasSeenStar
|
||||||
|
) {
|
||||||
|
indexName = indexName.strip();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Precedence:
|
||||||
|
* 1. Cannot mix cluster and selector strings.
|
||||||
|
* 2. Cluster string must be valid.
|
||||||
|
* 3. Index name must be valid.
|
||||||
|
* 4. Selector string must be valid.
|
||||||
|
*
|
||||||
|
* Since cluster string and/or selector string can be clubbed with the index name, we must try to
|
||||||
|
* manually extract them before we attempt to do #2, #3, and #4.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// It is possible to specify a pattern like "remote_cluster:index_name". Try to extract such details from the index string.
|
||||||
|
if (clusterString == null && selectorString == null) {
|
||||||
try {
|
try {
|
||||||
Tuple<String, String> splitPattern = IndexNameExpressionResolver.splitSelectorExpression(index);
|
var split = splitIndexName(indexName);
|
||||||
if (splitPattern.v2() != null) {
|
clusterString = split[0];
|
||||||
index = splitPattern.v1();
|
indexName = split[1];
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new ParsingException(e, source(ctx), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At the moment, selector strings for remote indices is not allowed.
|
||||||
|
if (clusterString != null && selectorString != null) {
|
||||||
|
throwOnMixingSelectorWithCluster(reassembleIndexName(clusterString, indexName, selectorString), ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation in the right precedence.
|
||||||
|
if (clusterString != null) {
|
||||||
|
clusterString = clusterString.strip();
|
||||||
|
validateClusterString(clusterString, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* It is possible for selector string to be attached to the index: "index_name::selector_string".
|
||||||
|
* Try to extract the selector string.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
Tuple<String, String> splitPattern = IndexNameExpressionResolver.splitSelectorExpression(indexName);
|
||||||
|
if (splitPattern.v2() != null) {
|
||||||
|
// Cluster string too was clubbed with the index name like selector string.
|
||||||
|
if (clusterString != null) {
|
||||||
|
throwOnMixingSelectorWithCluster(reassembleIndexName(clusterString, splitPattern.v1(), splitPattern.v2()), ctx);
|
||||||
|
} else {
|
||||||
|
// We've seen a selectorString. Use it.
|
||||||
|
selectorString = splitPattern.v2();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indexName = splitPattern.v1();
|
||||||
|
} catch (InvalidIndexNameException e) {
|
||||||
|
throw new ParsingException(e, source(ctx), e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveAndValidateIndex(indexName, ctx, hasSeenStar);
|
||||||
|
if (selectorString != null) {
|
||||||
|
selectorString = selectorString.strip();
|
||||||
|
try {
|
||||||
|
// Ensures that the selector provided is one of the valid kinds.
|
||||||
|
IndexNameExpressionResolver.SelectorResolver.validateIndexSelectorString(indexName, selectorString);
|
||||||
} catch (InvalidIndexNameException e) {
|
} catch (InvalidIndexNameException e) {
|
||||||
// throws exception if the selector expression is invalid. Selector resolution does not complain about exclusions
|
|
||||||
throw new ParsingException(e, source(ctx), e.getMessage());
|
throw new ParsingException(e, source(ctx), e.getMessage());
|
||||||
}
|
}
|
||||||
hasSeenStar = index.contains(WILDCARD) || hasSeenStar;
|
}
|
||||||
index = index.replace(WILDCARD, "").strip();
|
}
|
||||||
if (index.isBlank()) {
|
|
||||||
continue;
|
private static void resolveAndValidateIndex(String index, EsqlBaseParser.IndexPatternContext ctx, boolean hasSeenStar) {
|
||||||
|
// If index name is blank without any replacements, it was likely blank right from the beginning and is invalid.
|
||||||
|
if (index.isBlank()) {
|
||||||
|
throwInvalidIndexNameException(index, BLANK_INDEX_ERROR_MESSAGE, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSeenStar = hasSeenStar || index.contains(WILDCARD);
|
||||||
|
index = index.replace(WILDCARD, "").strip();
|
||||||
|
if (index.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var hasExclusion = index.startsWith(EXCLUSION);
|
||||||
|
index = removeExclusion(index);
|
||||||
|
String tempName;
|
||||||
|
try {
|
||||||
|
// remove the exclusion outside of <>, from index names with DateMath expression,
|
||||||
|
// e.g. -<-logstash-{now/d}> becomes <-logstash-{now/d}> before calling resolveDateMathExpression
|
||||||
|
tempName = IndexNameExpressionResolver.resolveDateMathExpression(index);
|
||||||
|
} catch (ElasticsearchParseException e) {
|
||||||
|
// throws exception if the DateMath expression is invalid, resolveDateMathExpression does not complain about exclusions
|
||||||
|
throw new ParsingException(e, source(ctx), e.getMessage());
|
||||||
|
}
|
||||||
|
hasExclusion = tempName.startsWith(EXCLUSION) || hasExclusion;
|
||||||
|
index = tempName.equals(index) ? index : removeExclusion(tempName);
|
||||||
|
try {
|
||||||
|
MetadataCreateIndexService.validateIndexOrAliasName(index, InvalidIndexNameException::new);
|
||||||
|
} catch (InvalidIndexNameException e) {
|
||||||
|
// ignore invalid index name if it has exclusions and there is an index with wildcard before it
|
||||||
|
if (hasSeenStar && hasExclusion) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
hasExclusion = index.startsWith(EXCLUSION);
|
|
||||||
index = removeExclusion(index);
|
InvalidIndexNameException errorToThrow = e;
|
||||||
String tempName;
|
/*
|
||||||
try {
|
* We only modify this particular message because it mentions '*' as an invalid char.
|
||||||
// remove the exclusion outside of <>, from index names with DateMath expression,
|
* However, we do allow asterisk in the index patterns: wildcarded patterns. Let's not
|
||||||
// e.g. -<-logstash-{now/d}> becomes <-logstash-{now/d}> before calling resolveDateMathExpression
|
* mislead the user by mentioning this char in the error message.
|
||||||
tempName = IndexNameExpressionResolver.resolveDateMathExpression(index);
|
*/
|
||||||
} catch (ElasticsearchParseException e) {
|
if (e.getMessage().contains("must not contain the following characters")) {
|
||||||
// throws exception if the DateMath expression is invalid, resolveDateMathExpression does not complain about exclusions
|
errorToThrow = new InvalidIndexNameException(index, e.getMessage().replace("'*',", ""));
|
||||||
throw new ParsingException(e, source(ctx), e.getMessage());
|
|
||||||
}
|
|
||||||
hasExclusion = tempName.startsWith(EXCLUSION) || hasExclusion;
|
|
||||||
index = tempName.equals(index) ? index : removeExclusion(tempName);
|
|
||||||
try {
|
|
||||||
MetadataCreateIndexService.validateIndexOrAliasName(index, InvalidIndexNameException::new);
|
|
||||||
} catch (InvalidIndexNameException e) {
|
|
||||||
// ignore invalid index name if it has exclusions and there is an index with wildcard before it
|
|
||||||
if (hasSeenStar && hasExclusion) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw new ParsingException(e, source(ctx), e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new ParsingException(errorToThrow, source(ctx), errorToThrow.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -472,10 +472,12 @@ public class StatementParserTests extends AbstractStatementParserTests {
|
||||||
"foo, test-*, abc, xyz", test123
|
"foo, test-*, abc, xyz", test123
|
||||||
""");
|
""");
|
||||||
assertStringAsIndexPattern("foo,test,xyz", command + " foo, test,xyz");
|
assertStringAsIndexPattern("foo,test,xyz", command + " foo, test,xyz");
|
||||||
|
assertStringAsIndexPattern("<logstash-{now/M{yyyy.MM}}>", command + " <logstash-{now/M{yyyy.MM}}>");
|
||||||
assertStringAsIndexPattern(
|
assertStringAsIndexPattern(
|
||||||
"<logstash-{now/M{yyyy.MM}}>,<logstash-{now/d{yyyy.MM.dd|+12:00}}>",
|
"<logstash-{now/M{yyyy.MM}}>,<logstash-{now/d{yyyy.MM.dd|+12:00}}>",
|
||||||
command + " <logstash-{now/M{yyyy.MM}}>, \"<logstash-{now/d{yyyy.MM.dd|+12:00}}>\""
|
command + " <logstash-{now/M{yyyy.MM}}>, \"<logstash-{now/d{yyyy.MM.dd|+12:00}}>\""
|
||||||
);
|
);
|
||||||
|
assertStringAsIndexPattern("<logstash-{now/d{yyyy.MM.dd|+12:00}}>", command + " \"<logstash-{now/d{yyyy.MM.dd|+12:00}}>\"");
|
||||||
assertStringAsIndexPattern(
|
assertStringAsIndexPattern(
|
||||||
"-<logstash-{now/M{yyyy.MM}}>,-<-logstash-{now/M{yyyy.MM}}>,"
|
"-<logstash-{now/M{yyyy.MM}}>,-<-logstash-{now/M{yyyy.MM}}>,"
|
||||||
+ "-<logstash-{now/d{yyyy.MM.dd|+12:00}}>,-<-logstash-{now/d{yyyy.MM.dd|+12:00}}>",
|
+ "-<logstash-{now/d{yyyy.MM.dd|+12:00}}>,-<-logstash-{now/d{yyyy.MM.dd|+12:00}}>",
|
||||||
|
@ -494,18 +496,30 @@ public class StatementParserTests extends AbstractStatementParserTests {
|
||||||
lineNumber,
|
lineNumber,
|
||||||
"mismatched input '\"index|pattern\"' expecting UNQUOTED_SOURCE"
|
"mismatched input '\"index|pattern\"' expecting UNQUOTED_SOURCE"
|
||||||
);
|
);
|
||||||
assertStringAsIndexPattern("*:index|pattern", command + " \"*:index|pattern\"");
|
// Entire index pattern is quoted. So it's not a parse error but a semantic error where the index name
|
||||||
|
// is invalid.
|
||||||
|
expectError(command + " \"*:index|pattern\"", "Invalid index name [index|pattern], must not contain the following characters");
|
||||||
clusterAndIndexAsIndexPattern(command, "cluster:index");
|
clusterAndIndexAsIndexPattern(command, "cluster:index");
|
||||||
clusterAndIndexAsIndexPattern(command, "cluster:.index");
|
clusterAndIndexAsIndexPattern(command, "cluster:.index");
|
||||||
clusterAndIndexAsIndexPattern(command, "cluster*:index*");
|
clusterAndIndexAsIndexPattern(command, "cluster*:index*");
|
||||||
clusterAndIndexAsIndexPattern(command, "cluster*:<logstash-{now/D}>*");// this is not a valid pattern, * should be inside <>
|
clusterAndIndexAsIndexPattern(command, "cluster*:<logstash-{now/d}>*");
|
||||||
clusterAndIndexAsIndexPattern(command, "cluster*:<logstash-{now/D}*>");
|
|
||||||
clusterAndIndexAsIndexPattern(command, "cluster*:*");
|
clusterAndIndexAsIndexPattern(command, "cluster*:*");
|
||||||
clusterAndIndexAsIndexPattern(command, "*:index*");
|
clusterAndIndexAsIndexPattern(command, "*:index*");
|
||||||
clusterAndIndexAsIndexPattern(command, "*:*");
|
clusterAndIndexAsIndexPattern(command, "*:*");
|
||||||
|
expectError(
|
||||||
|
command + " \"cluster:index|pattern\"",
|
||||||
|
"Invalid index name [index|pattern], must not contain the following characters"
|
||||||
|
);
|
||||||
|
expectError(command + " *:\"index|pattern\"", "expecting UNQUOTED_SOURCE");
|
||||||
if (EsqlCapabilities.Cap.INDEX_COMPONENT_SELECTORS.isEnabled()) {
|
if (EsqlCapabilities.Cap.INDEX_COMPONENT_SELECTORS.isEnabled()) {
|
||||||
assertStringAsIndexPattern("foo::data", command + " foo::data");
|
assertStringAsIndexPattern("foo::data", command + " foo::data");
|
||||||
assertStringAsIndexPattern("foo::failures", command + " foo::failures");
|
assertStringAsIndexPattern("foo::failures", command + " foo::failures");
|
||||||
|
expectErrorWithLineNumber(
|
||||||
|
command + " *,\"-foo\"::data",
|
||||||
|
"*,-foo::data",
|
||||||
|
lineNumber,
|
||||||
|
"mismatched input '::' expecting {<EOF>, '|', ',', 'metadata'}"
|
||||||
|
);
|
||||||
expectErrorWithLineNumber(
|
expectErrorWithLineNumber(
|
||||||
command + " cluster:\"foo::data\"",
|
command + " cluster:\"foo::data\"",
|
||||||
" cluster:\"foo::data\"",
|
" cluster:\"foo::data\"",
|
||||||
|
@ -585,6 +599,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
|
||||||
Map<String, String> commands = new HashMap<>();
|
Map<String, String> commands = new HashMap<>();
|
||||||
commands.put("FROM {}", "line 1:6: ");
|
commands.put("FROM {}", "line 1:6: ");
|
||||||
if (Build.current().isSnapshot()) {
|
if (Build.current().isSnapshot()) {
|
||||||
|
commands.put("TS {}", "line 1:4: ");
|
||||||
commands.put("ROW x = 1 | LOOKUP_🐔 {} ON j", "line 1:22: ");
|
commands.put("ROW x = 1 | LOOKUP_🐔 {} ON j", "line 1:22: ");
|
||||||
}
|
}
|
||||||
String lineNumber;
|
String lineNumber;
|
||||||
|
@ -625,7 +640,11 @@ public class StatementParserTests extends AbstractStatementParserTests {
|
||||||
expectInvalidIndexNameErrorWithLineNumber(command, "index::failure", lineNumber);
|
expectInvalidIndexNameErrorWithLineNumber(command, "index::failure", lineNumber);
|
||||||
|
|
||||||
// Cluster name cannot be combined with selector yet.
|
// Cluster name cannot be combined with selector yet.
|
||||||
var parseLineNumber = command.contains("FROM") ? 6 : 9;
|
int parseLineNumber = 6;
|
||||||
|
if (command.startsWith("TS")) {
|
||||||
|
parseLineNumber = 4;
|
||||||
|
}
|
||||||
|
|
||||||
expectDoubleColonErrorWithLineNumber(command, "cluster:foo::data", parseLineNumber + 11);
|
expectDoubleColonErrorWithLineNumber(command, "cluster:foo::data", parseLineNumber + 11);
|
||||||
expectDoubleColonErrorWithLineNumber(command, "cluster:foo::failures", parseLineNumber + 11);
|
expectDoubleColonErrorWithLineNumber(command, "cluster:foo::failures", parseLineNumber + 11);
|
||||||
|
|
||||||
|
@ -633,19 +652,18 @@ public class StatementParserTests extends AbstractStatementParserTests {
|
||||||
expectErrorWithLineNumber(
|
expectErrorWithLineNumber(
|
||||||
command,
|
command,
|
||||||
"cluster:\"foo\"::data",
|
"cluster:\"foo\"::data",
|
||||||
"line 1:14: ",
|
command.startsWith("FROM") ? "line 1:14: " : "line 1:12: ",
|
||||||
"mismatched input '\"foo\"' expecting UNQUOTED_SOURCE"
|
"mismatched input '\"foo\"' expecting UNQUOTED_SOURCE"
|
||||||
);
|
);
|
||||||
expectErrorWithLineNumber(
|
expectErrorWithLineNumber(
|
||||||
command,
|
command,
|
||||||
"cluster:\"foo\"::failures",
|
"cluster:\"foo\"::failures",
|
||||||
"line 1:14: ",
|
command.startsWith("FROM") ? "line 1:14: " : "line 1:12: ",
|
||||||
"mismatched input '\"foo\"' expecting UNQUOTED_SOURCE"
|
"mismatched input '\"foo\"' expecting UNQUOTED_SOURCE"
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Edge case that will be invalidated in follow up (https://github.com/elastic/elasticsearch/issues/122651)
|
expectDoubleColonErrorWithLineNumber(command, "\"cluster:foo\"::data", parseLineNumber + 13);
|
||||||
// expectDoubleColonErrorWithLineNumber(command, "\"cluster:foo\"::data", parseLineNumber + 13);
|
expectDoubleColonErrorWithLineNumber(command, "\"cluster:foo\"::failures", parseLineNumber + 13);
|
||||||
// expectDoubleColonErrorWithLineNumber(command, "\"cluster:foo\"::failures", parseLineNumber + 13);
|
|
||||||
|
|
||||||
expectErrorWithLineNumber(
|
expectErrorWithLineNumber(
|
||||||
command,
|
command,
|
||||||
|
@ -689,7 +707,7 @@ public class StatementParserTests extends AbstractStatementParserTests {
|
||||||
expectErrorWithLineNumber(
|
expectErrorWithLineNumber(
|
||||||
command,
|
command,
|
||||||
"cluster:\"index,index2\"::failures",
|
"cluster:\"index,index2\"::failures",
|
||||||
"line 1:14: ",
|
command.startsWith("FROM") ? "line 1:14: " : "line 1:12: ",
|
||||||
"mismatched input '\"index,index2\"' expecting UNQUOTED_SOURCE"
|
"mismatched input '\"index,index2\"' expecting UNQUOTED_SOURCE"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -746,16 +764,33 @@ public class StatementParserTests extends AbstractStatementParserTests {
|
||||||
clustersAndIndices(command, "index*", "-index#pattern");
|
clustersAndIndices(command, "index*", "-index#pattern");
|
||||||
clustersAndIndices(command, "*", "-<--logstash-{now/M{yyyy.MM}}>");
|
clustersAndIndices(command, "*", "-<--logstash-{now/M{yyyy.MM}}>");
|
||||||
clustersAndIndices(command, "index*", "-<--logstash#-{now/M{yyyy.MM}}>");
|
clustersAndIndices(command, "index*", "-<--logstash#-{now/M{yyyy.MM}}>");
|
||||||
|
expectInvalidIndexNameErrorWithLineNumber(command, "*, index#pattern", lineNumber, "index#pattern", "must not contain '#'");
|
||||||
|
expectInvalidIndexNameErrorWithLineNumber(
|
||||||
|
command,
|
||||||
|
"index*, index#pattern",
|
||||||
|
indexStarLineNumber,
|
||||||
|
"index#pattern",
|
||||||
|
"must not contain '#'"
|
||||||
|
);
|
||||||
|
expectDateMathErrorWithLineNumber(command, "cluster*:<logstash-{now/D}*>", commands.get(command), dateMathError);
|
||||||
expectDateMathErrorWithLineNumber(command, "*, \"-<-logstash-{now/D}>\"", lineNumber, dateMathError);
|
expectDateMathErrorWithLineNumber(command, "*, \"-<-logstash-{now/D}>\"", lineNumber, dateMathError);
|
||||||
expectDateMathErrorWithLineNumber(command, "*, -<-logstash-{now/D}>", lineNumber, dateMathError);
|
expectDateMathErrorWithLineNumber(command, "*, -<-logstash-{now/D}>", lineNumber, dateMathError);
|
||||||
expectDateMathErrorWithLineNumber(command, "\"*, -<-logstash-{now/D}>\"", commands.get(command), dateMathError);
|
expectDateMathErrorWithLineNumber(command, "\"*, -<-logstash-{now/D}>\"", commands.get(command), dateMathError);
|
||||||
expectDateMathErrorWithLineNumber(command, "\"*, -<-logst:ash-{now/D}>\"", commands.get(command), dateMathError);
|
expectDateMathErrorWithLineNumber(command, "\"*, -<-logst:ash-{now/D}>\"", commands.get(command), dateMathError);
|
||||||
if (EsqlCapabilities.Cap.INDEX_COMPONENT_SELECTORS.isEnabled()) {
|
if (EsqlCapabilities.Cap.INDEX_COMPONENT_SELECTORS.isEnabled()) {
|
||||||
clustersAndIndices(command, "*", "-index#pattern::data");
|
clustersAndIndices(command, "*", "-index::data");
|
||||||
clustersAndIndices(command, "*", "-index#pattern::data");
|
clustersAndIndices(command, "*", "-index::failures");
|
||||||
|
clustersAndIndices(command, "*", "-index*pattern::data");
|
||||||
|
clustersAndIndices(command, "*", "-index*pattern::failures");
|
||||||
|
|
||||||
|
// This is by existing design: refer to the comment in IdentifierBuilder#resolveAndValidateIndex() in the last
|
||||||
|
// catch clause. If there's an index with a wildcard before an invalid index, we don't error out.
|
||||||
clustersAndIndices(command, "index*", "-index#pattern::data");
|
clustersAndIndices(command, "index*", "-index#pattern::data");
|
||||||
clustersAndIndices(command, "*", "-<--logstash-{now/M{yyyy.MM}}>::data");
|
clustersAndIndices(command, "*", "-<--logstash-{now/M{yyyy.MM}}>::data");
|
||||||
clustersAndIndices(command, "index*", "-<--logstash#-{now/M{yyyy.MM}}>::data");
|
clustersAndIndices(command, "index*", "-<--logstash#-{now/M{yyyy.MM}}>::data");
|
||||||
|
|
||||||
|
expectError(command + "index1,<logstash-{now+-/d}>", "unit [-] not supported for date math [+-/d]");
|
||||||
|
|
||||||
// Throw on invalid date math
|
// Throw on invalid date math
|
||||||
expectDateMathErrorWithLineNumber(
|
expectDateMathErrorWithLineNumber(
|
||||||
command,
|
command,
|
||||||
|
@ -3140,6 +3175,122 @@ public class StatementParserTests extends AbstractStatementParserTests {
|
||||||
assertThat(joinType.coreJoin().joinName(), equalTo("LEFT OUTER"));
|
assertThat(joinType.coreJoin().joinName(), equalTo("LEFT OUTER"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testInvalidFromPatterns() {
|
||||||
|
var sourceCommands = new String[] { "FROM", "TS" };
|
||||||
|
var indexIsBlank = "Blank index specified in index pattern";
|
||||||
|
var remoteIsEmpty = "remote part is empty";
|
||||||
|
var invalidDoubleColonUsage = "invalid usage of :: separator";
|
||||||
|
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \" \"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \",,,\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \",,, \"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \", , ,,\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \",,,\",*", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*,\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*,,,\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"index1,,,,\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"index1,index2,,\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"index1,<-+^,index2\",*", "must not contain the following characters");
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"\",*", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*: ,*,\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*: ,*,\",validIndexName", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"\", \" \", \" \",validIndexName", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"index1\", \"index2\", \" ,index3,index4\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"index1,index2,,index3\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"index1,index2, ,index3\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*, \"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*\", \"\"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*\", \" \"", indexIsBlank);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*\", \":index1\"", remoteIsEmpty);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"index1,*,:index2\"", remoteIsEmpty);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*\", \"::data\"", remoteIsEmpty);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*\", \"::failures\"", remoteIsEmpty);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*,index1::\"", invalidDoubleColonUsage);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*\", index1, index2, \"index3:: \"", invalidDoubleColonUsage);
|
||||||
|
expectError(randomFrom(sourceCommands) + " \"*,index1::*\"", invalidDoubleColonUsage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testInvalidPatternsWithIntermittentQuotes() {
|
||||||
|
// There are 3 ways of crafting invalid index patterns that conforms to the grammar defined through ANTLR.
|
||||||
|
// 1. Not quoting the pattern,
|
||||||
|
// 2. Quoting individual patterns ("index1", "index2", ...), and,
|
||||||
|
// 3. Clubbing all the patterns into a single quoted string ("index1,index2,...).
|
||||||
|
//
|
||||||
|
// Note that in these tests, we unquote a pattern and then quote it immediately.
|
||||||
|
// This is because when randomly generating an index pattern, it may look like: "foo"::data.
|
||||||
|
// To convert it into a quoted string like "foo::data", we need to unquote and then re-quote it.
|
||||||
|
|
||||||
|
// Prohibited char in a quoted cross cluster index pattern should result in an error.
|
||||||
|
{
|
||||||
|
var randomIndex = randomIndexPattern();
|
||||||
|
// Select an invalid char to sneak in.
|
||||||
|
// Note: some chars like '|' and '"' are excluded to generate a proper invalid name.
|
||||||
|
Character[] invalidChars = { ' ', '/', '<', '>', '?' };
|
||||||
|
var randomInvalidChar = randomFrom(invalidChars);
|
||||||
|
|
||||||
|
// Construct the new invalid index pattern.
|
||||||
|
var remoteIndexWithInvalidChar = quote(randomIdentifier() + ":" + "foo" + randomInvalidChar + "bar");
|
||||||
|
var query = "FROM " + randomIndex + "," + remoteIndexWithInvalidChar;
|
||||||
|
expectError(query, "must not contain the following characters [' ','\"',',','/','<','>','?','\\','|']");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colon outside a quoted string should result in an ANTLR error: a comma is expected.
|
||||||
|
{
|
||||||
|
var randomIndex = randomIndexPattern();
|
||||||
|
|
||||||
|
// In the form of: "*|cluster alias:random string".
|
||||||
|
var malformedClusterAlias = quote((randomBoolean() ? "*" : randomIdentifier()) + ":" + randomIdentifier());
|
||||||
|
|
||||||
|
// We do not generate a cross cluster pattern or else we'd be getting a different error (which is tested in
|
||||||
|
// the next test).
|
||||||
|
var remoteIndex = quote(unquoteIndexPattern(randomIndexPattern(without(CROSS_CLUSTER))));
|
||||||
|
// Format: FROM <some index>, "<cluster alias: random string>":<remote index>
|
||||||
|
var query = "FROM " + randomIndex + "," + malformedClusterAlias + ":" + remoteIndex;
|
||||||
|
expectError(query, " mismatched input ':'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an explicit cluster string is present, then we expect an unquoted string next.
|
||||||
|
{
|
||||||
|
var randomIndex = randomIndexPattern();
|
||||||
|
var remoteClusterAlias = randomBoolean() ? "*" : randomIdentifier();
|
||||||
|
// In the form of: random string:random string.
|
||||||
|
var malformedRemoteIndex = quote(unquoteIndexPattern(randomIndexPattern(CROSS_CLUSTER)));
|
||||||
|
// Format: FROM <some index>, <cluster alias>:"random string:random string"
|
||||||
|
var query = "FROM " + randomIndex + "," + remoteClusterAlias + ":" + malformedRemoteIndex;
|
||||||
|
// Since "random string:random string" is partially quoted, expect a ANTLR's parse error.
|
||||||
|
expectError(query, "expecting UNQUOTED_SOURCE");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EsqlCapabilities.Cap.INDEX_COMPONENT_SELECTORS.isEnabled()) {
|
||||||
|
// If a stream in on a remote and the pattern is entirely quoted, we should be able to validate it.
|
||||||
|
// Note: invalid selector syntax is covered in a different test.
|
||||||
|
{
|
||||||
|
var fromPattern = randomIndexPattern();
|
||||||
|
var malformedIndexSelectorPattern = quote(
|
||||||
|
(randomIdentifier()) + ":" + unquoteIndexPattern(randomIndexPattern(INDEX_SELECTOR, without(CROSS_CLUSTER)))
|
||||||
|
);
|
||||||
|
// Format: FROM <some index>, "<cluster alias>:<some index>::<data|failures>"
|
||||||
|
var query = "FROM " + fromPattern + "," + malformedIndexSelectorPattern;
|
||||||
|
expectError(query, "Selectors are not yet supported on remote cluster patterns");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a stream in on a remote and the cluster alias and index pattern are separately quoted, we should
|
||||||
|
// still be able to validate it.
|
||||||
|
// Note: invalid selector syntax is covered in a different test.
|
||||||
|
{
|
||||||
|
var fromPattern = randomIndexPattern();
|
||||||
|
var malformedIndexSelectorPattern = quote(randomIdentifier())
|
||||||
|
+ ":"
|
||||||
|
+ quote(unquoteIndexPattern(randomIndexPattern(INDEX_SELECTOR, without(CROSS_CLUSTER))));
|
||||||
|
// Format: FROM <some index>, "<cluster alias>":"<some index>::<data|failures>"
|
||||||
|
var query = "FROM " + fromPattern + "," + malformedIndexSelectorPattern;
|
||||||
|
// Everything after "<cluster alias>" is extraneous input and hence ANTLR's error.
|
||||||
|
expectError(query, "mismatched input ':'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void testInvalidJoinPatterns() {
|
public void testInvalidJoinPatterns() {
|
||||||
assumeTrue("LOOKUP JOIN requires corresponding capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled());
|
assumeTrue("LOOKUP JOIN requires corresponding capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V12.isEnabled());
|
||||||
|
|
||||||
|
@ -3172,6 +3323,18 @@ public class StatementParserTests extends AbstractStatementParserTests {
|
||||||
|
|
||||||
// If one or more patterns participating in LOOKUP JOINs are partially quoted, we expect the partial quoting
|
// If one or more patterns participating in LOOKUP JOINs are partially quoted, we expect the partial quoting
|
||||||
// error messages to take precedence over any LOOKUP JOIN error messages.
|
// error messages to take precedence over any LOOKUP JOIN error messages.
|
||||||
|
|
||||||
|
{
|
||||||
|
// Generate a syntactically invalid (partial quoted) pattern.
|
||||||
|
var fromPatterns = quote(randomIdentifier()) + ":" + unquoteIndexPattern(randomIndexPattern(without(CROSS_CLUSTER)));
|
||||||
|
var joinPattern = randomIndexPattern();
|
||||||
|
expectError(
|
||||||
|
"FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(),
|
||||||
|
// Since the from pattern is partially quoted, we get an error at the end of the partially quoted string.
|
||||||
|
" mismatched input ':'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Generate a syntactically invalid (partial quoted) pattern.
|
// Generate a syntactically invalid (partial quoted) pattern.
|
||||||
var fromPatterns = randomIdentifier() + ":" + quote(randomIndexPatterns(without(CROSS_CLUSTER)));
|
var fromPatterns = randomIdentifier() + ":" + quote(randomIndexPatterns(without(CROSS_CLUSTER)));
|
||||||
|
@ -3184,6 +3347,17 @@ public class StatementParserTests extends AbstractStatementParserTests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var fromPatterns = randomIndexPattern();
|
||||||
|
// Generate a syntactically invalid (partial quoted) pattern.
|
||||||
|
var joinPattern = quote(randomIdentifier()) + ":" + unquoteIndexPattern(randomIndexPattern(without(CROSS_CLUSTER)));
|
||||||
|
expectError(
|
||||||
|
"FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(),
|
||||||
|
// Since the join pattern is partially quoted, we get an error at the end of the partially quoted string.
|
||||||
|
"mismatched input ':'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var fromPatterns = randomIndexPattern();
|
var fromPatterns = randomIndexPattern();
|
||||||
// Generate a syntactically invalid (partial quoted) pattern.
|
// Generate a syntactically invalid (partial quoted) pattern.
|
||||||
|
@ -3251,6 +3425,31 @@ public class StatementParserTests extends AbstractStatementParserTests {
|
||||||
+ "], index pattern selectors are not supported in LOOKUP JOIN"
|
+ "], index pattern selectors are not supported in LOOKUP JOIN"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Although we don't support selector strings for remote indices, it's alright.
|
||||||
|
// The parser error message takes precedence.
|
||||||
|
var fromPatterns = randomIndexPatterns();
|
||||||
|
var joinPattern = quote(randomIdentifier()) + "::" + randomFrom("data", "failures");
|
||||||
|
// After the end of the partially quoted string, i.e. the index name, parser now expects "ON..." and not a selector string.
|
||||||
|
expectError(
|
||||||
|
"FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(),
|
||||||
|
"mismatched input ':' expecting 'on'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Although we don't support selector strings for remote indices, it's alright.
|
||||||
|
// The parser error message takes precedence.
|
||||||
|
var fromPatterns = randomIndexPatterns();
|
||||||
|
var joinPattern = randomIdentifier() + "::" + quote(randomFrom("data", "failures"));
|
||||||
|
// After the index name and "::", parser expects an unquoted string, i.e. the selector string should not be
|
||||||
|
// partially quoted.
|
||||||
|
expectError(
|
||||||
|
"FROM " + fromPatterns + " | LOOKUP JOIN " + joinPattern + " ON " + randomIdentifier(),
|
||||||
|
" mismatched input ':' expecting UNQUOTED_SOURCE"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue