Skip to content

Commit 14e7037

Browse files
[8.19] ESQL: Disallow mixed quoted/unquoted patterns in FROM (#127636) (#129576)
* ESQL: Disallow mixed quoted/unquoted patterns in FROM (#127636) Disallow partial quoting: individual quoting of constituent strings in an index pattern --------- Co-authored-by: Pawan Kartik <[email protected]> (cherry picked from commit 9021040) --------- Co-authored-by: Alexander Spies <[email protected]>
1 parent 5249133 commit 14e7037

File tree

13 files changed

+1278
-1073
lines changed

13 files changed

+1278
-1073
lines changed

docs/changelog/127636.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
pr: 127636
2+
summary: Disallow mixed quoted/unquoted patterns in FROM
3+
area: ES|QL
4+
type: breaking
5+
issues:
6+
- 122651
7+
breaking:
8+
title: Disallow mixed quoted/unquoted patterns in FROM
9+
area: ES|QL
10+
details: "Previously, the ES|QL grammar allowed users to individually quote constituent strings in index patterns\
11+
\ such as \"remote_cluster\":\"index_name\". This would allow users to write complex malformed index patterns\
12+
\ that often slip through grammar and the subsequent validation. This could result in runtime errors\
13+
\ that can be misleading. This change simplifies the grammar to early reject such malformed index patterns\
14+
\ at the parsing stage, allowing users to write simpler queries and see more relevant and meaningful\
15+
\ errors."
16+
impact: "Users can write queries with simpler index patterns and see more meaningful and relevant errors."
17+
notable: false

x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,12 @@ static CsvSpecReader.CsvTestCase convertToRemoteIndices(CsvSpecReader.CsvTestCas
251251
String[] localIndices = fromStatement.substring("FROM ".length()).split(",");
252252
final String remoteIndices;
253253
if (canUseRemoteIndicesOnly() && randomBoolean()) {
254-
remoteIndices = Arrays.stream(localIndices).map(index -> "*:" + index.trim()).collect(Collectors.joining(","));
254+
remoteIndices = Arrays.stream(localIndices)
255+
.map(index -> unquoteAndRequoteAsRemote(index.trim(), true))
256+
.collect(Collectors.joining(","));
255257
} else {
256258
remoteIndices = Arrays.stream(localIndices)
257-
.map(index -> "*:" + index.trim() + "," + index.trim())
259+
.map(index -> unquoteAndRequoteAsRemote(index.trim(), false))
258260
.collect(Collectors.joining(","));
259261
}
260262
var newFrom = "FROM " + remoteIndices + " " + commands[0].substring(fromStatement.length());
@@ -265,9 +267,13 @@ static CsvSpecReader.CsvTestCase convertToRemoteIndices(CsvSpecReader.CsvTestCas
265267
assert parts.length >= 2 : commands[0];
266268
String[] indices = parts[1].split(",");
267269
if (canUseRemoteIndicesOnly() && randomBoolean()) {
268-
parts[1] = Arrays.stream(indices).map(index -> "*:" + index.trim()).collect(Collectors.joining(","));
270+
parts[1] = Arrays.stream(indices)
271+
.map(index -> unquoteAndRequoteAsRemote(index.trim(), true))
272+
.collect(Collectors.joining(","));
269273
} else {
270-
parts[1] = Arrays.stream(indices).map(index -> "*:" + index.trim() + "," + index.trim()).collect(Collectors.joining(","));
274+
parts[1] = Arrays.stream(indices)
275+
.map(index -> unquoteAndRequoteAsRemote(index.trim(), false))
276+
.collect(Collectors.joining(","));
271277
}
272278
String newNewMetrics = String.join(" ", parts);
273279
testCase.query = newNewMetrics + query.substring(first.length());
@@ -300,6 +306,40 @@ static boolean hasIndexMetadata(String query) {
300306
return false;
301307
}
302308

309+
/**
310+
* Since partial quoting is prohibited, we need to take the index name, unquote it,
311+
* convert it to a remote index, and then requote it. For example, "employees" is unquoted,
312+
* turned into the remote index *:employees, and then requoted to get "*:employees".
313+
* @param index Name of the index.
314+
* @param asRemoteIndexOnly If the return needs to be in the form of "*:idx,idx" or "*:idx".
315+
* @return A remote index pattern that's requoted.
316+
*/
317+
private static String unquoteAndRequoteAsRemote(String index, boolean asRemoteIndexOnly) {
318+
index = index.trim();
319+
320+
int numOfQuotes = 0;
321+
for (; numOfQuotes < index.length(); numOfQuotes++) {
322+
if (index.charAt(numOfQuotes) != '"') {
323+
break;
324+
}
325+
}
326+
327+
String unquoted = unquote(index, numOfQuotes);
328+
if (asRemoteIndexOnly) {
329+
return quote("*:" + unquoted, numOfQuotes);
330+
} else {
331+
return quote("*:" + unquoted + "," + unquoted, numOfQuotes);
332+
}
333+
}
334+
335+
private static String quote(String index, int numOfQuotes) {
336+
return "\"".repeat(numOfQuotes) + index + "\"".repeat(numOfQuotes);
337+
}
338+
339+
private static String unquote(String index, int numOfQuotes) {
340+
return index.substring(numOfQuotes, index.length() - numOfQuotes);
341+
}
342+
303343
@Override
304344
protected boolean enableRoundingDoubleValuesOnAsserting() {
305345
return true;

x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionIT.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,8 +1071,6 @@ public void testDataStreamPatterns() throws Exception {
10711071
testCases.put("test_ds_patterns*::data,test_ds_patterns*::failures,-test_ds_patterns_2*::data", 19L);
10721072
testCases.put("test_ds_patterns*::data,test_ds_patterns*::failures,-test_ds_patterns_2*::failures", 21L);
10731073

1074-
testCases.put("\"test_ds_patterns_1,test_ds_patterns_2\"::failures", 8L);
1075-
10761074
runDataStreamTest(testCases, new String[] { "test_ds_patterns_1", "test_ds_patterns_2", "test_ds_patterns_3" }, (key, value) -> {
10771075
try (var results = run("from " + key + " | stats count(@timestamp)")) {
10781076
assertEquals(key, 1, getValuesList(results).size());
@@ -1097,7 +1095,7 @@ public void testDataStreamInvalidPatterns() throws Exception {
10971095
// Only one selector separator is allowed per expression
10981096
testCases.put("::::data", "mismatched input '::' expecting {");
10991097
// Suffix case is not supported because there is no component named with the empty string
1100-
testCases.put("index::", "missing {QUOTED_STRING, UNQUOTED_SOURCE} at '|'");
1098+
testCases.put("index::", "missing UNQUOTED_SOURCE at '|'");
11011099

11021100
runDataStreamTest(testCases, new String[] { "test_ds_patterns_1" }, (key, value) -> {
11031101
logger.info(key);

x-pack/plugin/esql/src/main/antlr/EsqlBaseParser.g4

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,18 +150,21 @@ fromCommand
150150
;
151151

152152
indexPattern
153-
: (clusterString COLON)? indexString
154-
| indexString (CAST_OP selectorString)?
153+
: clusterString COLON unquotedIndexString
154+
| unquotedIndexString CAST_OP selectorString
155+
| indexString
155156
;
156157

157158
clusterString
158159
: UNQUOTED_SOURCE
159-
| QUOTED_STRING
160160
;
161161

162162
selectorString
163163
: UNQUOTED_SOURCE
164-
| QUOTED_STRING
164+
;
165+
166+
unquotedIndexString
167+
: UNQUOTED_SOURCE
165168
;
166169

167170
indexString

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

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)