Skip to content

Commit 9021040

Browse files
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]>
1 parent 5d19997 commit 9021040

File tree

13 files changed

+1323
-1135
lines changed

13 files changed

+1323
-1135
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
@@ -258,10 +258,12 @@ static CsvSpecReader.CsvTestCase convertToRemoteIndices(CsvSpecReader.CsvTestCas
258258
String[] localIndices = fromStatement.substring("FROM ".length()).split(",");
259259
final String remoteIndices;
260260
if (canUseRemoteIndicesOnly() && randomBoolean()) {
261-
remoteIndices = Arrays.stream(localIndices).map(index -> "*:" + index.trim()).collect(Collectors.joining(","));
261+
remoteIndices = Arrays.stream(localIndices)
262+
.map(index -> unquoteAndRequoteAsRemote(index.trim(), true))
263+
.collect(Collectors.joining(","));
262264
} else {
263265
remoteIndices = Arrays.stream(localIndices)
264-
.map(index -> "*:" + index.trim() + "," + index.trim())
266+
.map(index -> unquoteAndRequoteAsRemote(index.trim(), false))
265267
.collect(Collectors.joining(","));
266268
}
267269
var newFrom = "FROM " + remoteIndices + " " + commands[0].substring(fromStatement.length());
@@ -272,9 +274,13 @@ static CsvSpecReader.CsvTestCase convertToRemoteIndices(CsvSpecReader.CsvTestCas
272274
assert parts.length >= 2 : commands[0];
273275
String[] indices = parts[1].split(",");
274276
if (canUseRemoteIndicesOnly() && randomBoolean()) {
275-
parts[1] = Arrays.stream(indices).map(index -> "*:" + index.trim()).collect(Collectors.joining(","));
277+
parts[1] = Arrays.stream(indices)
278+
.map(index -> unquoteAndRequoteAsRemote(index.trim(), true))
279+
.collect(Collectors.joining(","));
276280
} else {
277-
parts[1] = Arrays.stream(indices).map(index -> "*:" + index.trim() + "," + index.trim()).collect(Collectors.joining(","));
281+
parts[1] = Arrays.stream(indices)
282+
.map(index -> unquoteAndRequoteAsRemote(index.trim(), false))
283+
.collect(Collectors.joining(","));
278284
}
279285
String newNewMetrics = String.join(" ", parts);
280286
testCase.query = newNewMetrics + query.substring(first.length());
@@ -307,6 +313,40 @@ static boolean hasIndexMetadata(String query) {
307313
return false;
308314
}
309315

316+
/**
317+
* Since partial quoting is prohibited, we need to take the index name, unquote it,
318+
* convert it to a remote index, and then requote it. For example, "employees" is unquoted,
319+
* turned into the remote index *:employees, and then requoted to get "*:employees".
320+
* @param index Name of the index.
321+
* @param asRemoteIndexOnly If the return needs to be in the form of "*:idx,idx" or "*:idx".
322+
* @return A remote index pattern that's requoted.
323+
*/
324+
private static String unquoteAndRequoteAsRemote(String index, boolean asRemoteIndexOnly) {
325+
index = index.trim();
326+
327+
int numOfQuotes = 0;
328+
for (; numOfQuotes < index.length(); numOfQuotes++) {
329+
if (index.charAt(numOfQuotes) != '"') {
330+
break;
331+
}
332+
}
333+
334+
String unquoted = unquote(index, numOfQuotes);
335+
if (asRemoteIndexOnly) {
336+
return quote("*:" + unquoted, numOfQuotes);
337+
} else {
338+
return quote("*:" + unquoted + "," + unquoted, numOfQuotes);
339+
}
340+
}
341+
342+
private static String quote(String index, int numOfQuotes) {
343+
return "\"".repeat(numOfQuotes) + index + "\"".repeat(numOfQuotes);
344+
}
345+
346+
private static String unquote(String index, int numOfQuotes) {
347+
return index.substring(numOfQuotes, index.length() - numOfQuotes);
348+
}
349+
310350
@Override
311351
protected boolean enableRoundingDoubleValuesOnAsserting() {
312352
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
@@ -1072,8 +1072,6 @@ public void testDataStreamPatterns() throws Exception {
10721072
testCases.put("test_ds_patterns*::data,test_ds_patterns*::failures,-test_ds_patterns_2*::data", 19L);
10731073
testCases.put("test_ds_patterns*::data,test_ds_patterns*::failures,-test_ds_patterns_2*::failures", 21L);
10741074

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

11031101
runDataStreamTest(testCases, new String[] { "test_ds_patterns_1" }, (key, value) -> {
11041102
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
@@ -108,18 +108,21 @@ indexPatternAndMetadataFields:
108108
;
109109

110110
indexPattern
111-
: (clusterString COLON)? indexString
112-
| indexString (CAST_OP selectorString)?
111+
: clusterString COLON unquotedIndexString
112+
| unquotedIndexString CAST_OP selectorString
113+
| indexString
113114
;
114115

115116
clusterString
116117
: UNQUOTED_SOURCE
117-
| QUOTED_STRING
118118
;
119119

120120
selectorString
121121
: UNQUOTED_SOURCE
122-
| QUOTED_STRING
122+
;
123+
124+
unquotedIndexString
125+
: UNQUOTED_SOURCE
123126
;
124127

125128
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)