Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2a05b89
enable CCS tests for subqueries
fang-xing-esql Nov 8, 2025
e1d0b40
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Nov 8, 2025
32751c8
fix tests
fang-xing-esql Nov 8, 2025
849060f
prune empty subquery when skipUnavailable=true
fang-xing-esql Nov 11, 2025
4555201
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Nov 11, 2025
8bc48bf
update cluster as skipped only if all index patterns associated with …
fang-xing-esql Nov 11, 2025
b22ce97
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Nov 11, 2025
76214e6
Update docs/changelog/137776.yaml
fang-xing-esql Nov 12, 2025
67c21e5
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Nov 12, 2025
6b48eab
update according to review comments
fang-xing-esql Nov 16, 2025
f46c2c4
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Nov 16, 2025
85d728e
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Nov 24, 2025
cc966bb
pick up EsqlQueryRequest change in main
fang-xing-esql Nov 25, 2025
2b46613
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Nov 25, 2025
490184f
remove the reference to IndicesExpressionGrouper when identify empty …
fang-xing-esql Nov 26, 2025
60cd5ef
add debug log for subqueries that do not have valid index patterns fo…
fang-xing-esql Nov 26, 2025
e1c210f
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Nov 26, 2025
5a8db0f
update according to review comments
fang-xing-esql Dec 1, 2025
3c17204
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Dec 1, 2025
e96a3f3
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Dec 2, 2025
feacb24
refactor according to review comments
fang-xing-esql Dec 2, 2025
786ac8f
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Dec 2, 2025
8feab3e
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Dec 3, 2025
2ecf4bf
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Dec 4, 2025
1ff8380
Merge branch 'main' into enable-subquery-ccs-tests
fang-xing-esql Dec 5, 2025
ede6f41
remove an ignored subquery CsvTests that causes trouble to mixed clus…
fang-xing-esql Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/137776.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 137776
summary: Enable CCS tests for subqueries
area: ES|QL
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,6 @@ protected void shouldSkipTest(String testName) throws IOException {
assumeFalse("LOOKUP JOIN after SORT not yet supported in CCS", testName.contains("OnTheCoordinator"));

assumeFalse("FORK not yet supported with CCS", testCase.requiredCapabilities.contains(FORK_V9.capabilityName()));

// And convertToRemoteIndices does not generate correct queries with subqueries in the FROM command yet
assumeFalse(
"Subqueries in FROM command not yet supported in CCS",
testCase.requiredCapabilities.contains(SUBQUERY_IN_FROM_COMMAND.capabilityName())
);
}

private TestFeatureService remoteFeaturesService() throws IOException {
Expand Down Expand Up @@ -298,6 +292,9 @@ static CsvSpecReader.CsvTestCase convertToRemoteIndices(CsvSpecReader.CsvTestCas
if (dataLocation == null) {
dataLocation = randomFrom(DataLocation.values());
}
if (testCase.requiredCapabilities.contains(SUBQUERY_IN_FROM_COMMAND.capabilityName())) {
return convertSubqueryToRemoteIndices(testCase);
}
String query = testCase.query;
// If true, we're using *:index, otherwise we're using *:index,index
boolean onlyRemotes = canUseRemoteIndicesOnly() && randomBoolean();
Expand Down Expand Up @@ -388,4 +385,13 @@ protected boolean supportsTDigestField() {
throw new RuntimeException(e);
}
}

/**
* Convert index patterns and subqueries in FROM commands to use remote indices for a given test case.
*/
private static CsvSpecReader.CsvTestCase convertSubqueryToRemoteIndices(CsvSpecReader.CsvTestCase testCase) {
String query = testCase.query;
testCase.query = EsqlTestUtils.convertSubqueryToRemoteIndices(query);
return testCase;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
import org.elasticsearch.index.mapper.blockloader.BlockLoaderFunctionConfig;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
import org.elasticsearch.search.SearchService;
import org.elasticsearch.search.aggregations.bucket.geogrid.GeoTileUtils;
import org.elasticsearch.search.crossproject.CrossProjectModeDecider;
Expand Down Expand Up @@ -225,6 +227,8 @@ public final class EsqlTestUtils {
public static final Literal FIVE = new Literal(Source.EMPTY, 5, DataType.INTEGER);
public static final Literal SIX = new Literal(Source.EMPTY, 6, DataType.INTEGER);

private static final Logger LOGGER = LogManager.getLogger(EsqlTestUtils.class);

public static Equals equalsOf(Expression left, Expression right) {
return new Equals(EMPTY, left, right, null);
}
Expand Down Expand Up @@ -1375,4 +1379,100 @@ private static String unquote(String index, int numOfQuotes) {
return index.substring(numOfQuotes, index.length() - numOfQuotes);
}

/**
* Convert index patterns and subqueries in FROM commands to use remote indices.
*/
public static String convertSubqueryToRemoteIndices(String testQuery) {
String query = testQuery;
// find the main from command, ignoring pipes inside subqueries
List<String> mainFromCommandAndTheRest = splitIgnoringParentheses(query, "|");
String mainFrom = mainFromCommandAndTheRest.get(0).strip();
List<String> theRest = mainFromCommandAndTheRest.size() > 1
? mainFromCommandAndTheRest.subList(1, mainFromCommandAndTheRest.size())
: List.of();
// check for metadata in the main from command
List<String> mainFromCommandWithMetadata = splitIgnoringParentheses(mainFrom, "metadata");
mainFrom = mainFromCommandWithMetadata.get(0).strip();
// if there is metadata, we need to add it back later
String metadata = mainFromCommandWithMetadata.size() > 1 ? " metadata " + mainFromCommandWithMetadata.get(1) : "";
// the main from command could be a comma separated list of index patterns, and subqueries
List<String> indexPatternsAndSubqueries = splitIgnoringParentheses(mainFrom, ",");
List<String> transformed = new ArrayList<>();
for (String indexPatternOrSubquery : indexPatternsAndSubqueries) {
// remove the from keyword if it's there
indexPatternOrSubquery = indexPatternOrSubquery.strip();
if (indexPatternOrSubquery.toLowerCase(Locale.ROOT).startsWith("from ")) {
indexPatternOrSubquery = indexPatternOrSubquery.strip().substring(5);
}
// substitute the index patterns or subquery with remote index patterns
if (isSubquery(indexPatternOrSubquery)) {
// it's a subquery, we need to process it recursively
String subquery = indexPatternOrSubquery.strip().substring(1, indexPatternOrSubquery.length() - 1);
String transformedSubquery = convertSubqueryToRemoteIndices(subquery);
transformed.add("(" + transformedSubquery + ")");
} else {
// It's an index pattern, we need to convert it to remote index pattern.
String remoteIndex = unquoteAndRequoteAsRemote(indexPatternOrSubquery, false);
transformed.add(remoteIndex);
}
}
// rebuild from command from transformed index patterns and subqueries
String transformedFrom = "FROM " + String.join(", ", transformed) + metadata;
// rebuild the whole query
mainFromCommandAndTheRest.set(0, transformedFrom);
testQuery = String.join(" | ", mainFromCommandAndTheRest);

LOGGER.trace("Transform query: \nFROM: {}\nTO: {}", query, testQuery);
return testQuery;
}

/**
* Checks if the given string is a subquery (enclosed in parentheses).
*/
private static boolean isSubquery(String indexPatternOrSubquery) {
String trimmed = indexPatternOrSubquery.strip();
return trimmed.startsWith("(") && trimmed.endsWith(")");
}

/**
* Splits the input string by the given delimiter, ignoring delimiters inside parentheses.
*/
public static List<String> splitIgnoringParentheses(String input, String delimiter) {
List<String> results = new ArrayList<>();
if (input == null || input.isEmpty()) return results;

int depth = 0; // parentheses nesting
int lastSplit = 0;
int delimiterLength = delimiter.length();

for (int i = 0; i <= input.length() - delimiterLength; i++) {
char c = input.charAt(i);

if (c == '(') {
depth++;
} else if (c == ')') {
if (depth > 0) depth--;
}

// check delimiter only outside parentheses
if (depth == 0) {
boolean match;
if (delimiter.length() == 1) {
match = c == delimiter.charAt(0);
} else {
match = input.regionMatches(true, i, delimiter, 0, delimiterLength);
}

if (match) {
results.add(input.substring(lastSplit, i).trim());
lastSplit = i + delimiterLength;
i += delimiterLength - 1; // skip the delimiter
}
}
}
// add remaining part
results.add(input.substring(lastSplit).trim());

return results;
}
}
Loading