2424import org .elasticsearch .test .rest .TestFeatureService ;
2525import org .elasticsearch .xpack .esql .CsvSpecReader ;
2626import org .elasticsearch .xpack .esql .CsvSpecReader .CsvTestCase ;
27+ import org .elasticsearch .xpack .esql .CsvTestsDataLoader ;
2728import org .elasticsearch .xpack .esql .SpecReader ;
2829import org .elasticsearch .xpack .esql .qa .rest .EsqlSpecTestCase ;
2930import org .junit .AfterClass ;
3940import java .util .List ;
4041import java .util .Locale ;
4142import java .util .Optional ;
43+ import java .util .Set ;
4244import java .util .regex .Pattern ;
4345import java .util .stream .Collectors ;
4446
4547import static org .elasticsearch .xpack .esql .CsvSpecReader .specParser ;
4648import static org .elasticsearch .xpack .esql .CsvTestUtils .isEnabled ;
49+ import static org .elasticsearch .xpack .esql .CsvTestsDataLoader .CSV_DATASET_MAP ;
4750import static org .elasticsearch .xpack .esql .CsvTestsDataLoader .ENRICH_SOURCE_INDICES ;
4851import static org .elasticsearch .xpack .esql .EsqlTestUtils .classpathResources ;
52+ import static org .elasticsearch .xpack .esql .action .EsqlCapabilities .Cap .ENABLE_LOOKUP_JOIN_ON_REMOTE ;
4953import static org .elasticsearch .xpack .esql .action .EsqlCapabilities .Cap .FORK_V9 ;
5054import static org .elasticsearch .xpack .esql .action .EsqlCapabilities .Cap .INLINESTATS ;
5155import static org .elasticsearch .xpack .esql .action .EsqlCapabilities .Cap .INLINESTATS_V2 ;
@@ -109,6 +113,22 @@ public MultiClusterSpecIT(
109113 super (fileName , groupName , testName , lineNumber , convertToRemoteIndices (testCase ), instructions , mode );
110114 }
111115
116+ // TODO: think how to handle this better
117+ public static final Set <String > NO_REMOTE_LOOKUP_JOIN_TESTS = Set .of (
118+ // Lookup join after STATS is not supported in CCS yet
119+ "StatsAndLookupIPAndMessageFromIndex" ,
120+ "JoinMaskingRegex" ,
121+ "StatsAndLookupIPFromIndex" ,
122+ "StatsAndLookupMessageFromIndex" ,
123+ "MvJoinKeyOnTheLookupIndexAfterStats" ,
124+ "MvJoinKeyOnFromAfterStats" ,
125+ // Lookup join after SORT is not supported in CCS yet
126+ "NullifiedJoinKeyToPurgeTheJoin" ,
127+ "SortBeforeAndAfterJoin" ,
128+ "SortEvalBeforeLookup" ,
129+ "SortBeforeAndAfterMultipleJoinAndMvExpand"
130+ );
131+
112132 @ Override
113133 protected void shouldSkipTest (String testName ) throws IOException {
114134 boolean remoteMetadata = testCase .requiredCapabilities .contains (METADATA_FIELDS_REMOTE_TEST .capabilityName ());
@@ -129,10 +149,20 @@ protected void shouldSkipTest(String testName) throws IOException {
129149 assumeFalse ("INLINESTATS not yet supported in CCS" , testCase .requiredCapabilities .contains (INLINESTATS_V2 .capabilityName ()));
130150 assumeFalse ("INLINESTATS not yet supported in CCS" , testCase .requiredCapabilities .contains (JOIN_PLANNING_V1 .capabilityName ()));
131151 assumeFalse ("INLINESTATS not yet supported in CCS" , testCase .requiredCapabilities .contains (INLINESTATS_V8 .capabilityName ()));
132- assumeFalse ("LOOKUP JOIN not yet supported in CCS" , testCase .requiredCapabilities .contains (JOIN_LOOKUP_V12 .capabilityName ()));
152+ if (testCase .requiredCapabilities .contains (JOIN_LOOKUP_V12 .capabilityName ())) {
153+ assumeTrue ("LOOKUP JOIN not yet supported in CCS" , hasCapabilities (List .of (ENABLE_LOOKUP_JOIN_ON_REMOTE .capabilityName ())));
154+ }
133155 // Unmapped fields require a coorect capability response from every cluster, which isn't currently implemented.
134156 assumeFalse ("UNMAPPED FIELDS not yet supported in CCS" , testCase .requiredCapabilities .contains (UNMAPPED_FIELDS .capabilityName ()));
135157 assumeFalse ("FORK not yet supported in CCS" , testCase .requiredCapabilities .contains (FORK_V9 .capabilityName ()));
158+ // Tests that use capabilities not supported in CCS
159+ assumeFalse (
160+ "This syntax is not supported with remote LOOKUP JOIN" ,
161+ NO_REMOTE_LOOKUP_JOIN_TESTS .stream ().anyMatch (testName ::contains )
162+ );
163+ // Tests that do SORT before LOOKUP JOIN - not supported in CCS
164+ assumeFalse ("LOOKUP JOIN after SORT not yet supported in CCS" , testName .contains ("OnTheCoordinator" ));
165+
136166 }
137167
138168 @ Override
@@ -181,6 +211,19 @@ protected RestClient buildClient(Settings settings, HttpHost[] localHosts) throw
181211 // These indices are used in metadata tests so we want them on remote only for consistency
182212 public static final List <String > METADATA_INDICES = List .of ("employees" , "apps" , "ul_logs" );
183213
214+ // These are lookup indices, we want them on both remotes and locals
215+ public static final Set <String > LOOKUP_INDICES = CSV_DATASET_MAP .values ()
216+ .stream ()
217+ .filter (td -> td .settingFileName () != null && td .settingFileName ().equals ("lookup-settings.json" ))
218+ .map (CsvTestsDataLoader .TestDataset ::indexName )
219+ .collect (Collectors .toSet ());
220+
221+ public static final Set <String > LOOKUP_ENDPOINTS = LOOKUP_INDICES .stream ().map (i -> "/" + i + "/_bulk" ).collect (Collectors .toSet ());
222+
223+ public static final Set <String > ENRICH_ENDPOINTS = ENRICH_SOURCE_INDICES .stream ()
224+ .map (i -> "/" + i + "/_bulk" )
225+ .collect (Collectors .toSet ());
226+
184227 /**
185228 * Creates a new mock client that dispatches every request to both the local and remote clusters, excluding _bulk and _query requests.
186229 * - '_bulk' requests are randomly sent to either the local or remote cluster to populate data. Some spec tests, such as AVG,
@@ -199,15 +242,17 @@ static RestClient twoClients(RestClient localClient, RestClient remoteClient) th
199242 return localClient .performRequest (request );
200243 } else if (endpoint .endsWith ("/_bulk" ) && METADATA_INDICES .stream ().anyMatch (i -> endpoint .equals ("/" + i + "/_bulk" ))) {
201244 return remoteClient .performRequest (request );
202- } else if (endpoint .endsWith ("/_bulk" ) && ENRICH_SOURCE_INDICES .stream ().noneMatch (i -> endpoint .equals ("/" + i + "/_bulk" ))) {
203- return bulkClient .performRequest (request );
204- } else {
205- Request [] clones = cloneRequests (request , 2 );
206- Response resp1 = remoteClient .performRequest (clones [0 ]);
207- Response resp2 = localClient .performRequest (clones [1 ]);
208- assertEquals (resp1 .getStatusLine ().getStatusCode (), resp2 .getStatusLine ().getStatusCode ());
209- return resp2 ;
210- }
245+ } else if (endpoint .endsWith ("/_bulk" )
246+ && ENRICH_ENDPOINTS .contains (endpoint ) == false
247+ && LOOKUP_ENDPOINTS .contains (endpoint ) == false ) {
248+ return bulkClient .performRequest (request );
249+ } else {
250+ Request [] clones = cloneRequests (request , 2 );
251+ Response resp1 = remoteClient .performRequest (clones [0 ]);
252+ Response resp2 = localClient .performRequest (clones [1 ]);
253+ assertEquals (resp1 .getStatusLine ().getStatusCode (), resp2 .getStatusLine ().getStatusCode ());
254+ return resp2 ;
255+ }
211256 });
212257 doAnswer (invocation -> {
213258 IOUtils .close (localClient , remoteClient );
@@ -251,37 +296,32 @@ static CsvSpecReader.CsvTestCase convertToRemoteIndices(CsvSpecReader.CsvTestCas
251296 String query = testCase .query ;
252297 String [] commands = query .split ("\\ |" );
253298 String first = commands [0 ].trim ();
299+ // If true, we're using *:index, otherwise we're using *:index,index
300+ boolean onlyRemotes = canUseRemoteIndicesOnly () && randomBoolean ();
254301 if (commands [0 ].toLowerCase (Locale .ROOT ).startsWith ("from" )) {
255302 String [] parts = commands [0 ].split ("(?i)metadata" );
256303 assert parts .length >= 1 : parts ;
257304 String fromStatement = parts [0 ];
258305 String [] localIndices = fromStatement .substring ("FROM " .length ()).split ("," );
259- final String remoteIndices ;
260- if (canUseRemoteIndicesOnly () && randomBoolean ()) {
261- remoteIndices = Arrays .stream (localIndices )
262- .map (index -> unquoteAndRequoteAsRemote (index .trim (), true ))
263- .collect (Collectors .joining ("," ));
264- } else {
265- remoteIndices = Arrays .stream (localIndices )
266- .map (index -> unquoteAndRequoteAsRemote (index .trim (), false ))
267- .collect (Collectors .joining ("," ));
306+ if (Arrays .stream (localIndices ).anyMatch (i -> LOOKUP_INDICES .contains (i .trim ().toLowerCase (Locale .ROOT )))) {
307+ // If the query contains lookup indices, use only remotes to avoid duplication
308+ onlyRemotes = true ;
268309 }
310+ final boolean onlyRemotesFinal = onlyRemotes ;
311+ final String remoteIndices = Arrays .stream (localIndices )
312+ .map (index -> unquoteAndRequoteAsRemote (index .trim (), onlyRemotesFinal ))
313+ .collect (Collectors .joining ("," ));
269314 var newFrom = "FROM " + remoteIndices + " " + commands [0 ].substring (fromStatement .length ());
270315 testCase .query = newFrom + query .substring (first .length ());
271316 }
272317 if (commands [0 ].toLowerCase (Locale .ROOT ).startsWith ("ts " )) {
273318 String [] parts = commands [0 ].split ("\\ s+" );
274319 assert parts .length >= 2 : commands [0 ];
275320 String [] indices = parts [1 ].split ("," );
276- if (canUseRemoteIndicesOnly () && randomBoolean ()) {
277- parts [1 ] = Arrays .stream (indices )
278- .map (index -> unquoteAndRequoteAsRemote (index .trim (), true ))
279- .collect (Collectors .joining ("," ));
280- } else {
281- parts [1 ] = Arrays .stream (indices )
282- .map (index -> unquoteAndRequoteAsRemote (index .trim (), false ))
283- .collect (Collectors .joining ("," ));
284- }
321+ final boolean onlyRemotesFinal = onlyRemotes ;
322+ parts [1 ] = Arrays .stream (indices )
323+ .map (index -> unquoteAndRequoteAsRemote (index .trim (), onlyRemotesFinal ))
324+ .collect (Collectors .joining ("," ));
285325 String newNewMetrics = String .join (" " , parts );
286326 testCase .query = newNewMetrics + query .substring (first .length ());
287327 }
@@ -359,9 +399,7 @@ protected boolean supportsInferenceTestService() {
359399
360400 @ Override
361401 protected boolean supportsIndexModeLookup () throws IOException {
362- // CCS does not yet support JOIN_LOOKUP_V10 and clusters falsely report they have this capability
363- // return hasCapabilities(List.of(JOIN_LOOKUP_V10.capabilityName()));
364- return false ;
402+ return hasCapabilities (List .of (JOIN_LOOKUP_V12 .capabilityName ()));
365403 }
366404
367405 @ Override
0 commit comments