Skip to content

Commit 733eee4

Browse files
[ES|QL] Enable CCS tests for subqueries (elastic#137776)
* enable CCS tests for subqueries
1 parent 765b2b9 commit 733eee4

File tree

13 files changed

+1446
-405
lines changed

13 files changed

+1446
-405
lines changed

docs/changelog/137776.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 137776
2+
summary: Enable CCS tests for subqueries
3+
area: ES|QL
4+
type: enhancement
5+
issues: []

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,6 @@ protected void shouldSkipTest(String testName) throws IOException {
168168
assumeFalse("LOOKUP JOIN after SORT not yet supported in CCS", testName.contains("OnTheCoordinator"));
169169

170170
assumeFalse("FORK not yet supported with CCS", testCase.requiredCapabilities.contains(FORK_V9.capabilityName()));
171-
172-
// And convertToRemoteIndices does not generate correct queries with subqueries in the FROM command yet
173-
assumeFalse(
174-
"Subqueries in FROM command not yet supported in CCS",
175-
testCase.requiredCapabilities.contains(SUBQUERY_IN_FROM_COMMAND.capabilityName())
176-
);
177171
}
178172

179173
private TestFeatureService remoteFeaturesService() throws IOException {
@@ -298,6 +292,9 @@ static CsvSpecReader.CsvTestCase convertToRemoteIndices(CsvSpecReader.CsvTestCas
298292
if (dataLocation == null) {
299293
dataLocation = randomFrom(DataLocation.values());
300294
}
295+
if (testCase.requiredCapabilities.contains(SUBQUERY_IN_FROM_COMMAND.capabilityName())) {
296+
return convertSubqueryToRemoteIndices(testCase);
297+
}
301298
String query = testCase.query;
302299
// If true, we're using *:index, otherwise we're using *:index,index
303300
boolean onlyRemotes = canUseRemoteIndicesOnly() && randomBoolean();
@@ -388,4 +385,13 @@ protected boolean supportsTDigestField() {
388385
throw new RuntimeException(e);
389386
}
390387
}
388+
389+
/**
390+
* Convert index patterns and subqueries in FROM commands to use remote indices for a given test case.
391+
*/
392+
private static CsvSpecReader.CsvTestCase convertSubqueryToRemoteIndices(CsvSpecReader.CsvTestCase testCase) {
393+
String query = testCase.query;
394+
testCase.query = EsqlTestUtils.convertSubqueryToRemoteIndices(query);
395+
return testCase;
396+
}
391397
}

x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
import org.elasticsearch.index.mapper.blockloader.BlockLoaderFunctionConfig;
6161
import org.elasticsearch.index.shard.ShardId;
6262
import org.elasticsearch.license.XPackLicenseState;
63+
import org.elasticsearch.logging.LogManager;
64+
import org.elasticsearch.logging.Logger;
6365
import org.elasticsearch.search.SearchService;
6466
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils;
6567
import org.elasticsearch.search.crossproject.CrossProjectModeDecider;
@@ -225,6 +227,8 @@ public final class EsqlTestUtils {
225227
public static final Literal FIVE = new Literal(Source.EMPTY, 5, DataType.INTEGER);
226228
public static final Literal SIX = new Literal(Source.EMPTY, 6, DataType.INTEGER);
227229

230+
private static final Logger LOGGER = LogManager.getLogger(EsqlTestUtils.class);
231+
228232
public static Equals equalsOf(Expression left, Expression right) {
229233
return new Equals(EMPTY, left, right, null);
230234
}
@@ -1375,4 +1379,100 @@ private static String unquote(String index, int numOfQuotes) {
13751379
return index.substring(numOfQuotes, index.length() - numOfQuotes);
13761380
}
13771381

1382+
/**
1383+
* Convert index patterns and subqueries in FROM commands to use remote indices.
1384+
*/
1385+
public static String convertSubqueryToRemoteIndices(String testQuery) {
1386+
String query = testQuery;
1387+
// find the main from command, ignoring pipes inside subqueries
1388+
List<String> mainFromCommandAndTheRest = splitIgnoringParentheses(query, "|");
1389+
String mainFrom = mainFromCommandAndTheRest.get(0).strip();
1390+
List<String> theRest = mainFromCommandAndTheRest.size() > 1
1391+
? mainFromCommandAndTheRest.subList(1, mainFromCommandAndTheRest.size())
1392+
: List.of();
1393+
// check for metadata in the main from command
1394+
List<String> mainFromCommandWithMetadata = splitIgnoringParentheses(mainFrom, "metadata");
1395+
mainFrom = mainFromCommandWithMetadata.get(0).strip();
1396+
// if there is metadata, we need to add it back later
1397+
String metadata = mainFromCommandWithMetadata.size() > 1 ? " metadata " + mainFromCommandWithMetadata.get(1) : "";
1398+
// the main from command could be a comma separated list of index patterns, and subqueries
1399+
List<String> indexPatternsAndSubqueries = splitIgnoringParentheses(mainFrom, ",");
1400+
List<String> transformed = new ArrayList<>();
1401+
for (String indexPatternOrSubquery : indexPatternsAndSubqueries) {
1402+
// remove the from keyword if it's there
1403+
indexPatternOrSubquery = indexPatternOrSubquery.strip();
1404+
if (indexPatternOrSubquery.toLowerCase(Locale.ROOT).startsWith("from ")) {
1405+
indexPatternOrSubquery = indexPatternOrSubquery.strip().substring(5);
1406+
}
1407+
// substitute the index patterns or subquery with remote index patterns
1408+
if (isSubquery(indexPatternOrSubquery)) {
1409+
// it's a subquery, we need to process it recursively
1410+
String subquery = indexPatternOrSubquery.strip().substring(1, indexPatternOrSubquery.length() - 1);
1411+
String transformedSubquery = convertSubqueryToRemoteIndices(subquery);
1412+
transformed.add("(" + transformedSubquery + ")");
1413+
} else {
1414+
// It's an index pattern, we need to convert it to remote index pattern.
1415+
String remoteIndex = unquoteAndRequoteAsRemote(indexPatternOrSubquery, false);
1416+
transformed.add(remoteIndex);
1417+
}
1418+
}
1419+
// rebuild from command from transformed index patterns and subqueries
1420+
String transformedFrom = "FROM " + String.join(", ", transformed) + metadata;
1421+
// rebuild the whole query
1422+
mainFromCommandAndTheRest.set(0, transformedFrom);
1423+
testQuery = String.join(" | ", mainFromCommandAndTheRest);
1424+
1425+
LOGGER.trace("Transform query: \nFROM: {}\nTO: {}", query, testQuery);
1426+
return testQuery;
1427+
}
1428+
1429+
/**
1430+
* Checks if the given string is a subquery (enclosed in parentheses).
1431+
*/
1432+
private static boolean isSubquery(String indexPatternOrSubquery) {
1433+
String trimmed = indexPatternOrSubquery.strip();
1434+
return trimmed.startsWith("(") && trimmed.endsWith(")");
1435+
}
1436+
1437+
/**
1438+
* Splits the input string by the given delimiter, ignoring delimiters inside parentheses.
1439+
*/
1440+
public static List<String> splitIgnoringParentheses(String input, String delimiter) {
1441+
List<String> results = new ArrayList<>();
1442+
if (input == null || input.isEmpty()) return results;
1443+
1444+
int depth = 0; // parentheses nesting
1445+
int lastSplit = 0;
1446+
int delimiterLength = delimiter.length();
1447+
1448+
for (int i = 0; i <= input.length() - delimiterLength; i++) {
1449+
char c = input.charAt(i);
1450+
1451+
if (c == '(') {
1452+
depth++;
1453+
} else if (c == ')') {
1454+
if (depth > 0) depth--;
1455+
}
1456+
1457+
// check delimiter only outside parentheses
1458+
if (depth == 0) {
1459+
boolean match;
1460+
if (delimiter.length() == 1) {
1461+
match = c == delimiter.charAt(0);
1462+
} else {
1463+
match = input.regionMatches(true, i, delimiter, 0, delimiterLength);
1464+
}
1465+
1466+
if (match) {
1467+
results.add(input.substring(lastSplit, i).trim());
1468+
lastSplit = i + delimiterLength;
1469+
i += delimiterLength - 1; // skip the delimiter
1470+
}
1471+
}
1472+
}
1473+
// add remaining part
1474+
results.add(input.substring(lastSplit).trim());
1475+
1476+
return results;
1477+
}
13781478
}

0 commit comments

Comments
 (0)